This is an automated email from the ASF dual-hosted git repository.
JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 8c50293d308 Add daemon and cleanup tools for IoTDB Edge (#18552)
8c50293d308 is described below
commit 8c50293d3083b47a110e5c4a9825062b6a2c0c2d
Author: Jackie Tien <[email protected]>
AuthorDate: Mon Aug 31 18:09:19 2026 +0800
Add daemon and cleanup tools for IoTDB Edge (#18552)
---
.github/scripts/test-edge-ops.py | 466 ++++++++++++++++++++++++++++
.github/workflows/edge-it.yml | 4 +
distribution/src/assembly/all.xml | 1 +
distribution/src/assembly/datanode.xml | 1 +
distribution/src/assembly/edge.xml | 11 +
iotdb-core/datanode/src/assembly/server.xml | 1 +
scripts/sbin/windows/stop-edge.bat | 9 +-
scripts/tools/ops/daemon-edge.sh | 114 +++++++
scripts/tools/ops/destroy-edge.sh | 161 ++++++++++
scripts/tools/windows/ops/destroy-edge.bat | 96 ++++++
10 files changed, 861 insertions(+), 3 deletions(-)
diff --git a/.github/scripts/test-edge-ops.py b/.github/scripts/test-edge-ops.py
new file mode 100644
index 00000000000..ef16fd04ca8
--- /dev/null
+++ b/.github/scripts/test-edge-ops.py
@@ -0,0 +1,466 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""Run Edge ops against disposable data and stubbed service/stop commands."""
+
+import fnmatch
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+import xml.etree.ElementTree as ET
+
+REPOSITORY = Path(__file__).resolve().parents[2]
+WINDOWS = os.name == "nt"
+DIRECTORY_KEYS = (
+ "cn_system_dir",
+ "cn_consensus_dir",
+ "dn_system_dir",
+ "dn_data_dirs",
+ "dn_consensus_dir",
+ "dn_wal_dirs",
+ "dn_tracing_dir",
+ "dn_sync_dir",
+ "pipe_receiver_file_dirs",
+ "iot_consensus_v2_receiver_file_dirs",
+ "sort_tmp_dir",
+)
+
+
+class EdgeOpsTest(unittest.TestCase):
+ def setUp(self):
+ self.temporary = tempfile.TemporaryDirectory(prefix="iotdb-edge-ops-")
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name).resolve()
+ self.installation = self.root / "edge installation"
+ self.config = self.installation / "conf"
+ self.config.mkdir(parents=True)
+ self.events = self.root / "events.txt"
+ self.bin = self.root / "bin"
+ self.bin.mkdir()
+ self.env = os.environ.copy()
+ for key in (
+ "IOTDB_HOME",
+ "IOTDB_CONF",
+ "IOTDB_DATA_HOME",
+ "IOTDB_LOG_DIR",
+ "JAVA_HOME",
+ ):
+ self.env.pop(key, None)
+ self.env["PATH"] = str(self.bin) + os.pathsep + self.env["PATH"]
+ self.env["EDGE_TEST_EVENTS"] = str(self.events)
+ self.env["EDGE_TEST_STOP_STATUS"] = "0"
+ self.env["SYSTEMD_DIR"] = str(self.root / "systemd")
+ Path(self.env["SYSTEMD_DIR"]).mkdir()
+ self.marker = self.directory(self.installation / "data") / "marker"
+ self.marker.touch()
+ self.keep = self.directory(self.installation / "logs") / "keep.log"
+ self.keep.touch()
+ if WINDOWS:
+ self.destroy = self.copy("tools/windows/ops/destroy-edge.bat")
+ self.stop = self.installation / "sbin/windows/stop-edge.bat"
+ self.write(
+ self.stop,
+ "@echo off\n"
+ '>>"%EDGE_TEST_EVENTS%" echo stop %*\n'
+ 'if not exist "%IOTDB_HOME%\\data\\marker" exit /b 97\n'
+ "exit /b %EDGE_TEST_STOP_STATUS%\n",
+ )
+ else:
+ self.destroy = self.copy("tools/ops/destroy-edge.sh")
+ self.daemon = self.copy("tools/ops/daemon-edge.sh")
+ self.stop = self.installation / "sbin/stop-edge.sh"
+ self.write(
+ self.stop,
+ "#!/bin/bash\n"
+ 'printf "stop %s\\n" "$*" >> "$EDGE_TEST_EVENTS"\n'
+ "sleep 0.05\n"
+ '[ -f "$IOTDB_HOME/data/marker" ] || exit 97\n'
+ 'exit "$EDGE_TEST_STOP_STATUS"\n',
+ )
+ self.write(
+ self.installation / "sbin/start-edge.sh", "#!/bin/bash\nexit
0\n"
+ )
+ self.write(self.bin / "java", "#!/bin/bash\nexit 0\n")
+ self.write(
+ self.bin / "systemctl",
+ "#!/bin/bash\n"
+ 'printf "systemctl %s\\n" "$*" >> "$EDGE_TEST_EVENTS"\n'
+ 'if [ "$1" = show ]; then printf "%s\\n"
"$EDGE_TEST_SERVICE_PIDFILE"; fi\n'
+ '[ "$1" != "$EDGE_TEST_SYSTEMCTL_FAIL" ]\n',
+ )
+
+ @staticmethod
+ def write(path, content):
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_bytes(content.encode("utf-8"))
+ path.chmod(0o755)
+
+ def copy(self, relative):
+ destination = self.installation / relative
+ destination.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copyfile(REPOSITORY / "scripts" / relative, destination)
+ destination.chmod(0o755)
+ return destination
+
+ @staticmethod
+ def directory(path):
+ path.mkdir(parents=True, exist_ok=True)
+ (path / "data.txt").touch()
+ return path
+
+ def properties(self, lines, name="iotdb-system.properties"):
+ self.write(self.config / name, "\r\n".join(lines) + "\r\n")
+
+ def run_script(self, script, arguments=(), answer=""):
+ if WINDOWS:
+ command = '"{}" /d /c call "{}" {}'.format(
+ os.environ["COMSPEC"], script, " ".join(arguments)
+ )
+ else:
+ command = ["bash", str(script), *arguments]
+ return subprocess.run(
+ command,
+ input=answer,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ errors="replace",
+ env=self.env,
+ cwd=self.root,
+ timeout=30,
+ )
+
+ def assert_success(self, result):
+ self.assertEqual(result.returncode, 0, result.stdout)
+
+ def event_lines(self):
+ return self.events.read_text().splitlines() if self.events.exists()
else []
+
+ def test_default_answer_and_no_do_not_stop_or_delete(self):
+ for answer in ("", "\n", "n\n", "yes\n"):
+ with self.subTest(answer=answer):
+ self.env["CLEAN_SERVICE"] = "y"
+ self.assert_success(self.run_script(self.destroy,
answer=answer))
+ self.assertTrue(self.marker.exists())
+ self.assertEqual(self.event_lines(), [])
+
+ def test_invalid_arguments_do_not_stop_or_delete(self):
+ for arguments in (("--unknown",), ("-f", "extra")):
+ with self.subTest(arguments=arguments):
+ result = self.run_script(self.destroy, arguments)
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertTrue(self.marker.exists())
+ self.assertEqual(self.event_lines(), [])
+
+ def test_force_cleans_defaults_from_an_unrelated_working_directory(self):
+ tracing = self.directory(self.installation / "datanode/tracing")
+ result = self.run_script(self.destroy, ("-f",))
+ self.assert_success(result)
+ self.assertFalse((self.installation / "data").exists())
+ self.assertFalse(tracing.exists())
+ self.assertTrue(self.keep.exists())
+ self.assertTrue(self.config.exists())
+ self.assertIn("stop -f", self.event_lines())
+ self.assertIn("IoTDB Edge clean done", result.stdout)
+
+ def test_yes_confirms_cleanup(self):
+ self.assert_success(self.run_script(self.destroy, answer="Y\n"))
+ self.assertFalse(self.marker.exists())
+
+ def test_stop_failure_preserves_all_data(self):
+ self.env["EDGE_TEST_STOP_STATUS"] = "42"
+ result = self.run_script(self.destroy, ("-f",))
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertTrue(self.marker.exists())
+ self.assertNotIn("clean done", result.stdout)
+
+ def test_all_configured_directories_and_multiple_tiers(self):
+ targets = []
+ lines = ["# dn_data_dirs=ignored", "! cn_system_dir=ignored"]
+ for index, key in enumerate(DIRECTORY_KEYS):
+ base = self.installation if index % 2 else self.root
+ target = self.directory(base / (key + " with spaces=[1]"))
+ value = target.relative_to(base) if base == self.installation else
target
+ lines.append(" {} = {} ".format(key, value.as_posix()))
+ targets.append(target)
+ tiers = [
+ self.directory(self.installation / "tier one"),
+ self.directory(self.root / "absolute tier"),
+ self.directory(self.installation / "tier=two"),
+ ]
+ lines.append(
+ " dn_data_dirs = tier one ; {}, tier=two
".format(tiers[1].as_posix())
+ )
+ # The previous value of a repeated property must not be removed.
+ superseded = targets.pop(DIRECTORY_KEYS.index("dn_data_dirs"))
+ self.properties(lines)
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ for target in targets + tiers:
+ self.assertFalse(target.exists(), str(target))
+ self.assertTrue(superseded.exists())
+ self.assertTrue((self.config / "iotdb-system.properties").exists())
+ self.assertTrue(self.keep.exists())
+
+ def test_legacy_configuration_files(self):
+ cn = self.directory(self.root / "legacy cn")
+ dn = self.directory(self.root / "legacy dn")
+ self.properties(
+ ["cn_system_dir=" + cn.as_posix()], "iotdb-confignode.properties"
+ )
+ self.properties(["dn_data_dirs=" + dn.as_posix()],
"iotdb-datanode.properties")
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ self.assertFalse(cn.exists())
+ self.assertFalse(dn.exists())
+
+ def test_custom_home_and_configuration(self):
+ self.env["IOTDB_HOME"] = str(self.installation)
+ self.config = self.root / "custom configuration"
+ self.config.mkdir()
+ self.env["IOTDB_CONF"] = str(self.config)
+ target = self.directory(self.root / "custom data")
+ self.properties(["dn_data_dirs=" + target.as_posix()])
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ self.assertFalse(target.exists())
+
+ def test_unified_configuration_takes_precedence(self):
+ untouched = self.directory(self.root / "legacy data")
+ self.properties(
+ ["dn_data_dirs=" + untouched.as_posix()],
"iotdb-datanode.properties"
+ )
+ self.properties(["# Defaults"])
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ self.assertTrue(untouched.exists())
+
+ def test_home_or_parent_is_rejected_before_any_deletion(self):
+ # Only disposable paths are used even if the guard regresses.
+ for value in (".", "data/..", ".."):
+ with self.subTest(value=value):
+ self.properties(["dn_data_dirs=" + value])
+ result = self.run_script(self.destroy, ("-f",))
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertIn("Refusing to remove", result.stdout)
+ self.assertTrue(self.marker.exists())
+ self.assertTrue(self.keep.exists())
+
+ def test_missing_stop_script_preserves_data(self):
+ self.stop.unlink()
+ result = self.run_script(self.destroy, ("-f",))
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertTrue(self.marker.exists())
+
+ def test_empty_property_uses_default_not_an_earlier_value(self):
+ untouched = self.directory(self.root / "superseded data")
+ self.properties(["dn_data_dirs=" + untouched.as_posix(),
"dn_data_dirs= "])
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ self.assertFalse(self.marker.exists())
+ self.assertTrue(untouched.exists())
+
+ @unittest.skipIf(WINDOWS, "Unix symbolic links")
+ def test_symlinked_parent_cannot_hide_a_home_directory(self):
+ (self.root / "home alias").symlink_to(
+ self.installation, target_is_directory=True
+ )
+ self.properties(
+ ["dn_data_dirs=" + (self.root / "home alias/data/..").as_posix()]
+ )
+ result = self.run_script(self.destroy, ("-f",))
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertIn("Refusing to remove", result.stdout)
+ self.assertTrue(self.marker.exists())
+
+ @unittest.skipIf(WINDOWS, "Unix deletion errors")
+ def test_deletion_failure_is_not_reported_as_success(self):
+ self.write(self.bin / "rm", "#!/bin/bash\nexit 42\n")
+ result = self.run_script(self.destroy, ("-f",))
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertNotIn("clean done", result.stdout)
+ self.assertTrue(self.marker.exists())
+
+ @unittest.skipIf(
+ WINDOWS, "The Unix launcher supports a separate DataNode data home"
+ )
+ def test_data_home_does_not_change_confignode_home(self):
+ data_home = self.directory(self.root / "external data home")
+ self.env["IOTDB_DATA_HOME"] = str(data_home)
+ cn = self.directory(self.installation / "cn")
+ dn = self.directory(data_home / "dn")
+ keep_cn = self.directory(data_home / "cn")
+ keep_dn = self.directory(self.installation / "dn")
+ self.properties(["cn_system_dir=cn", "dn_data_dirs=dn"])
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ self.assertFalse(cn.exists())
+ self.assertFalse(dn.exists())
+ self.assertTrue(keep_cn.exists())
+ self.assertTrue(keep_dn.exists())
+
+ @unittest.skipIf(WINDOWS, "Unix systemd integration")
+ def test_cleanup_stops_only_the_matching_systemd_service(self):
+ self.env["EDGE_TEST_SERVICE_PIDFILE"] = str(self.installation /
"edge.pid")
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ events = self.event_lines()
+ self.assertLess(
+ events.index("systemctl stop iotdb-edge"), events.index("stop -f")
+ )
+
+ @unittest.skipIf(WINDOWS, "Unix systemd integration")
+ def test_other_systemd_installation_is_not_stopped(self):
+ self.env["EDGE_TEST_SERVICE_PIDFILE"] = str(self.root / "another
edge/edge.pid")
+ self.assert_success(self.run_script(self.destroy, ("-f",)))
+ self.assertNotIn("systemctl stop iotdb-edge", self.event_lines())
+
+ @unittest.skipIf(WINDOWS, "Unix systemd integration")
+ def test_systemd_stop_failure_preserves_data(self):
+ self.env["EDGE_TEST_SERVICE_PIDFILE"] = str(self.installation /
"edge.pid")
+ self.env["EDGE_TEST_SYSTEMCTL_FAIL"] = "stop"
+ result = self.run_script(self.destroy, ("-f",))
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertTrue(self.marker.exists())
+ self.assertNotIn("stop -f", self.event_lines())
+
+ @unittest.skipIf(WINDOWS, "Unix systemd registration")
+ def test_daemon_tracks_the_forked_jvm_and_supports_java_home(self):
+ java_home = self.root / "custom java"
+ self.write(java_home / "bin/java", "#!/bin/bash\nexit 0\n")
+ self.env["JAVA_HOME"] = str(java_home)
+ result = self.run_script(self.daemon, answer="\n\n")
+ self.assert_success(result)
+ unit_path = Path(self.env["SYSTEMD_DIR"]) / "iotdb-edge.service"
+ unit = unit_path.read_text()
+ for setting in (
+ "Type=forking",
+ "PIDFile={}/edge.pid".format(self.installation),
+ "Restart=on-failure",
+ "SuccessExitStatus=143",
+ "RestartPreventExitStatus=SIGKILL",
+ "LimitNOFILE=65536",
+ "StartLimitIntervalSec=600s",
+ "StartLimitBurst=3",
+ 'Environment="JAVA_HOME={}"'.format(java_home),
+ 'Environment="PATH={}/bin:'.format(java_home),
+ 'ExecStart="{}/sbin/start-edge.sh"'.format(self.installation),
+ 'ExecStop="{}/sbin/stop-edge.sh"'.format(self.installation),
+ ):
+ self.assertIn(setting, unit)
+ self.assertEqual(
+ self.event_lines(),
+ [
+ "systemctl daemon-reload",
+ "systemctl stop iotdb-edge",
+ "stop ",
+ "systemctl start iotdb-edge",
+ "systemctl enable iotdb-edge",
+ ],
+ )
+ if shutil.which("systemd-analyze"):
+ check = subprocess.run(
+ ["systemd-analyze", "verify", str(unit_path)],
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ timeout=30,
+ )
+ self.assertEqual(check.returncode, 0, check.stdout)
+
+ @unittest.skipIf(WINDOWS, "Unix systemd registration")
+ def test_daemon_path_java_and_declining_start_and_enable(self):
+ self.assert_success(self.run_script(self.daemon, answer="n\nn\n"))
+ self.assertEqual(self.event_lines(), ["systemctl daemon-reload"])
+ unit = (Path(self.env["SYSTEMD_DIR"]) /
"iotdb-edge.service").read_text()
+ self.assertIn('Environment="JAVA_HOME="', unit)
+ self.assertIn('Environment="PATH={}"'.format(self.env["PATH"]), unit)
+
+ @unittest.skipIf(WINDOWS, "Unix systemd registration")
+ def test_daemon_rejects_invalid_java_home(self):
+ self.env["JAVA_HOME"] = str(self.root / "missing java")
+ result = self.run_script(self.daemon, answer="n\nn\n")
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertEqual(self.event_lines(), [])
+
+ @unittest.skipIf(WINDOWS, "Unix systemd registration")
+ def test_daemon_propagates_service_start_errors(self):
+ self.env["EDGE_TEST_SYSTEMCTL_FAIL"] = "start"
+ result = self.run_script(self.daemon, answer="y\ny\n")
+ self.assertNotEqual(result.returncode, 0, result.stdout)
+ self.assertNotIn("systemctl enable iotdb-edge", self.event_lines())
+
+ @unittest.skipUnless(WINDOWS, "Native Windows stop script")
+ def test_windows_force_stop_returns_without_pausing(self):
+ self.copy("sbin/windows/stop-edge.bat")
+ result = self.run_script(self.stop, ("-f",))
+ self.assert_success(result)
+ self.assertNotIn("Press any key", result.stdout)
+
+
+class AssemblyTest(unittest.TestCase):
+ @staticmethod
+ def packaged(descriptor, path):
+ root = ET.parse(REPOSITORY / descriptor).getroot()
+ for file_set in root.findall("./fileSets/fileSet"):
+ directory = file_set.findtext("directory", "")
+ if not directory.endswith("/scripts/tools"):
+ continue
+ includes = [node.text for node in
file_set.findall("./includes/include")]
+ excludes = [node.text for node in
file_set.findall("./excludes/exclude")]
+ if (
+ not includes or any(fnmatch.fnmatchcase(path, p) for p in
includes)
+ ) and not any(fnmatch.fnmatchcase(path, p) for p in excludes):
+ return file_set.findtext("fileMode")
+ return None
+
+ def test_edge_ops_are_executable_and_only_in_edge_packages(self):
+ for path in (
+ "ops/daemon-edge.sh",
+ "ops/destroy-edge.sh",
+ "windows/ops/destroy-edge.bat",
+ ):
+ self.assertEqual(
+ self.packaged("distribution/src/assembly/edge.xml", path),
"0755"
+ )
+ for descriptor in (
+ "distribution/src/assembly/all.xml",
+ "distribution/src/assembly/datanode.xml",
+ "distribution/src/assembly/confignode.xml",
+ "iotdb-core/datanode/src/assembly/server.xml",
+ "iotdb-core/confignode/src/assembly/confignode.xml",
+ ):
+ self.assertIsNone(
+ self.packaged(descriptor, path), descriptor + ": " + path
+ )
+
+ def test_edge_does_not_restore_standalone_ops(self):
+ paths = ["ops/daemon-{}.sh".format(node) for node in ("confignode",
"datanode")]
+ for node in ("all", "confignode", "datanode"):
+ paths.extend(
+ (
+ "ops/destroy-{}.sh".format(node),
+ "windows/ops/destroy-{}.bat".format(node),
+ )
+ )
+ for path in paths:
+ self.assertIsNone(
+ self.packaged("distribution/src/assembly/edge.xml", path), path
+ )
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/.github/workflows/edge-it.yml b/.github/workflows/edge-it.yml
index 64158417011..e4adfdd9f0c 100644
--- a/.github/workflows/edge-it.yml
+++ b/.github/workflows/edge-it.yml
@@ -42,6 +42,8 @@ jobs:
steps:
- uses: actions/checkout@v5
+ - name: Test Edge ops scripts
+ run: python3 .github/scripts/test-edge-ops.py
- name: Set up JDK
uses: actions/setup-java@v5
with:
@@ -89,6 +91,8 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
+ - name: Test Edge ops scripts
+ run: python .github/scripts/test-edge-ops.py
- name: Test Edge Windows launchers
shell: powershell
run: .github/scripts/test-edge-windows.ps1
diff --git a/distribution/src/assembly/all.xml
b/distribution/src/assembly/all.xml
index 610e53edbfc..25662a5fe95 100644
--- a/distribution/src/assembly/all.xml
+++ b/distribution/src/assembly/all.xml
@@ -101,6 +101,7 @@
<excludes>
<exclude>*ainode.*</exclude>
<exclude>**/*ainode.*</exclude>
+ <exclude>**/*edge.*</exclude>
</excludes>
<fileMode>0755</fileMode>
</fileSet>
diff --git a/distribution/src/assembly/datanode.xml
b/distribution/src/assembly/datanode.xml
index d3996da9786..0cb7b66a280 100644
--- a/distribution/src/assembly/datanode.xml
+++ b/distribution/src/assembly/datanode.xml
@@ -73,6 +73,7 @@
<exclude>**/*confignode.*</exclude>
<exclude>**/*all.*</exclude>
<exclude>**/*ainode.*</exclude>
+ <exclude>**/*edge.*</exclude>
</excludes>
<fileMode>0755</fileMode>
</fileSet>
diff --git a/distribution/src/assembly/edge.xml
b/distribution/src/assembly/edge.xml
index 54c598e80b5..67eed08bec3 100644
--- a/distribution/src/assembly/edge.xml
+++ b/distribution/src/assembly/edge.xml
@@ -125,6 +125,17 @@
</excludes>
<fileMode>0755</fileMode>
</fileSet>
+ <!-- Edge replacements for the standalone node daemon and cleanup
tools. -->
+ <fileSet>
+ <outputDirectory>tools</outputDirectory>
+ <directory>${project.basedir}/../scripts/tools</directory>
+ <includes>
+ <include>ops/daemon-edge.sh</include>
+ <include>ops/destroy-edge.sh</include>
+ <include>windows/ops/destroy-edge.bat</include>
+ </includes>
+ <fileMode>0755</fileMode>
+ </fileSet>
</fileSets>
<files>
<file>
diff --git a/iotdb-core/datanode/src/assembly/server.xml
b/iotdb-core/datanode/src/assembly/server.xml
index 2e1dab2e584..50a25bbce9e 100644
--- a/iotdb-core/datanode/src/assembly/server.xml
+++ b/iotdb-core/datanode/src/assembly/server.xml
@@ -70,6 +70,7 @@
<exclude>**/*confignode.*</exclude>
<exclude>**/*all.*</exclude>
<exclude>**/*ainode.*</exclude>
+ <exclude>**/*edge.*</exclude>
</excludes>
<fileMode>0755</fileMode>
</fileSet>
diff --git a/scripts/sbin/windows/stop-edge.bat
b/scripts/sbin/windows/stop-edge.bat
index 4eed010be62..2253f5354c9 100644
--- a/scripts/sbin/windows/stop-edge.bat
+++ b/scripts/sbin/windows/stop-edge.bat
@@ -18,9 +18,12 @@
@REM under the License.
@REM
+setlocal
echo Stopping IoTDB Edge (the merged ConfigNode + DataNode process)
-pushd %~dp0\..\..
+pushd "%~dp0\..\.."
set "IOTDB_HOME=%cd%"
popd
-powershell -NoProfile -Command "$plain='-DIOTDB_HOME=' + $env:IOTDB_HOME;
$quoted='-DIOTDB_HOME=' + [char]34 + $env:IOTDB_HOME + [char]34;
Get-CimInstance Win32_Process -Filter \"name='java.exe'\" | Where-Object {
$line=$_.CommandLine; $sameHome=$line -and ($line.Contains($plain + ' ') -or
$line.EndsWith($plain) -or $line.Contains($quoted + ' ') -or
$line.EndsWith($quoted)); $sameHome -and
$line.Contains('org.apache.iotdb.edge.EdgeNode') } | ForEach-Object {
Stop-Process -Id $_.ProcessId [...]
-pause
+powershell -NoProfile -Command "$ErrorActionPreference='Stop';
$plain='-DIOTDB_HOME=' + $env:IOTDB_HOME; $quoted='-DIOTDB_HOME=' + [char]34 +
$env:IOTDB_HOME + [char]34; Get-CimInstance Win32_Process -Filter
\"name='java.exe'\" | Where-Object { $line=$_.CommandLine; $sameHome=$line -and
($line.Contains($plain + ' ') -or $line.EndsWith($plain) -or
$line.Contains($quoted + ' ') -or $line.EndsWith($quoted)); $sameHome -and
$line.Contains('org.apache.iotdb.edge.EdgeNode') } | ForEach-Object [...]
+set "STOP_STATUS=%errorlevel%"
+if not "%~1"=="-f" pause
+exit /b %STOP_STATUS%
diff --git a/scripts/tools/ops/daemon-edge.sh b/scripts/tools/ops/daemon-edge.sh
new file mode 100644
index 00000000000..060ab203192
--- /dev/null
+++ b/scripts/tools/ops/daemon-edge.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+set -e
+
+IOTDB_HOME="${IOTDB_HOME:-$(cd "$(dirname "$0")"/../.. && pwd)}"
+IOTDB_HOME="$(cd "$IOTDB_HOME" && pwd -P)"
+export IOTDB_HOME
+IOTDB_SBIN_HOME="$IOTDB_HOME/sbin"
+SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}"
+
+if [ ! -d "$SYSTEMD_DIR" ] || ! command -v systemctl >/dev/null 2>&1; then
+ echo "Current system can't support systemd."
+ exit 1
+fi
+
+JAVA=java
+if [ -n "$JAVA_HOME" ]; then
+ JAVA="$JAVA_HOME/bin/java"
+ if [ -x "$JAVA_HOME/bin/amd64/java" ]; then
+ JAVA="$JAVA_HOME/bin/amd64/java"
+ fi
+fi
+if ! "$JAVA" --version >/dev/null 2>&1; then
+ echo "Java is not available. Please check JAVA_HOME and PATH."
+ exit 1
+fi
+
+if [ ! -x "$IOTDB_SBIN_HOME/start-edge.sh" ] || [ ! -x
"$IOTDB_SBIN_HOME/stop-edge.sh" ]; then
+ echo "Cannot find the executable Edge start/stop scripts in
$IOTDB_SBIN_HOME."
+ exit 1
+fi
+
+# Quote environment values and executable paths using systemd's syntax.
+systemd_quote() {
+ local value="$1"
+ value="${value//\\/\\\\}"
+ value="${value//\"/\\\"}"
+ value="${value//%/%%}"
+ printf '"%s"' "$value"
+}
+
+FILE_NAME="$SYSTEMD_DIR/iotdb-edge.service"
+cat > "$FILE_NAME" <<EOF
+[Unit]
+Description=iotdb-edge
+Documentation=https://iotdb.apache.org/
+After=network.target
+StartLimitIntervalSec=600s
+StartLimitBurst=3
+
+[Service]
+StandardOutput=null
+StandardError=null
+LimitNOFILE=65536
+# start-edge.sh forks the JVM and records its PID before returning.
+Type=forking
+PIDFile=${IOTDB_HOME//%/%%}/edge.pid
+WorkingDirectory=${IOTDB_HOME//%/%%}
+User=root
+Group=root
+Environment=$(systemd_quote "JAVA_HOME=$JAVA_HOME")
+Environment=$(systemd_quote "PATH=${JAVA_HOME:+$JAVA_HOME/bin:}$PATH")
+Environment=$(systemd_quote "IOTDB_HOME=$IOTDB_HOME")
+Environment=$(systemd_quote "IOTDB_CONF=${IOTDB_CONF:-$IOTDB_HOME/conf}")
+Environment=$(systemd_quote "IOTDB_DATA_HOME=${IOTDB_DATA_HOME:-$IOTDB_HOME}")
+Environment=$(systemd_quote "IOTDB_LOG_DIR=${IOTDB_LOG_DIR:-$IOTDB_HOME/logs}")
+ExecStart=$(systemd_quote "$IOTDB_SBIN_HOME/start-edge.sh")
+ExecStop=$(systemd_quote "$IOTDB_SBIN_HOME/stop-edge.sh")
+Restart=on-failure
+SuccessExitStatus=143
+RestartSec=5
+RestartPreventExitStatus=SIGKILL
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl daemon-reload
+echo "Daemon service of IoTDB Edge has been successfully registered."
+echo
+echo "Do you want to execute 'systemctl start iotdb-edge'? y/n (default y)"
+read -r START_SERVICE || START_SERVICE=""
+if [[ -z "$START_SERVICE" || "$START_SERVICE" =~ ^[Yy]$ ]]; then
+ # Stop both an existing service and a manually started Edge before
starting.
+ systemctl stop iotdb-edge
+ "$IOTDB_SBIN_HOME/stop-edge.sh"
+ systemctl start iotdb-edge
+ echo "Executed successfully."
+fi
+echo
+echo "Do you want to execute 'systemctl enable iotdb-edge' to start at boot?
y/n (default y)"
+read -r ADD_STARTUP || ADD_STARTUP=""
+if [[ -z "$ADD_STARTUP" || "$ADD_STARTUP" =~ ^[Yy]$ ]]; then
+ systemctl enable iotdb-edge
+ echo "Executed successfully."
+fi
diff --git a/scripts/tools/ops/destroy-edge.sh
b/scripts/tools/ops/destroy-edge.sh
new file mode 100644
index 00000000000..980be9c25fa
--- /dev/null
+++ b/scripts/tools/ops/destroy-edge.sh
@@ -0,0 +1,161 @@
+#!/bin/bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+set -e
+
+if [ "$#" -gt 1 ] || { [ "$#" -eq 1 ] && [ "$1" != "-f" ]; }; then
+ echo "Usage: $0 [-f]"
+ exit 1
+fi
+if [ "${1:-}" != "-f" ]; then
+ read -r -p "Do you want to clean all the data of IoTDB Edge? y/n (default
n): " CLEAN_SERVICE || CLEAN_SERVICE=""
+ if [[ "$CLEAN_SERVICE" != "y" && "$CLEAN_SERVICE" != "Y" ]]; then
+ echo "Exiting..."
+ exit 0
+ fi
+fi
+
+IOTDB_HOME="${IOTDB_HOME:-$(cd "$(dirname "$0")"/../.. && pwd)}"
+IOTDB_HOME="$(cd "$IOTDB_HOME" && pwd -P)"
+export IOTDB_HOME
+IOTDB_CONF="${IOTDB_CONF:-$IOTDB_HOME/conf}"
+IOTDB_DATA_HOME="${IOTDB_DATA_HOME:-$IOTDB_HOME}"
+if [ -d "$IOTDB_DATA_HOME" ]; then
+ IOTDB_DATA_HOME="$(cd "$IOTDB_DATA_HOME" && pwd -P)"
+fi
+
+if [ ! -f "$IOTDB_HOME/sbin/stop-edge.sh" ]; then
+ echo "Cannot find $IOTDB_HOME/sbin/stop-edge.sh. No data has been removed."
+ exit 1
+fi
+
+# Stop the matching service first so Restart=on-failure cannot race with
cleanup.
+if command -v systemctl >/dev/null 2>&1 &&
+ [ "$(systemctl show --property=PIDFile --value iotdb-edge.service
2>/dev/null)" = "$IOTDB_HOME/edge.pid" ]; then
+ systemctl stop iotdb-edge
+fi
+bash "$IOTDB_HOME/sbin/stop-edge.sh" -f
+
+if [ -f "$IOTDB_CONF/iotdb-system.properties" ]; then
+ CN_CONFIG="$IOTDB_CONF/iotdb-system.properties"
+ DN_CONFIG="$CN_CONFIG"
+else
+ CN_CONFIG="$IOTDB_CONF/iotdb-confignode.properties"
+ DN_CONFIG="$IOTDB_CONF/iotdb-datanode.properties"
+fi
+
+read_property() {
+ local config="$1" key="$2" fallback="$3"
+ if [ ! -f "$config" ]; then
+ printf '%s\n' "$fallback"
+ return
+ fi
+ # Keep spaces and '=' in values; the last active property wins, as in Java.
+ awk -v key="$key" -v fallback="$fallback" '
+ {
+ separator = index($0, "=")
+ if (!separator) next
+ name = substr($0, 1, separator - 1)
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", name)
+ if (name == key) {
+ value = substr($0, separator + 1)
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
+ }
+ }
+ END { print (value == "" ? fallback : value) }
+ ' "$config"
+}
+
+CLEAN_PATHS=()
+add_paths() {
+ local directories="$1" base="$2" path protected
+ local paths=()
+ IFS=';,' read -r -a paths <<< "$directories"
+ for path in "${paths[@]}"; do
+ path="${path#"${path%%[![:space:]]*}"}"
+ path="${path%"${path##*[![:space:]]}"}"
+ [ -n "$path" ] || continue
+ case "$path" in
+ *://*|OBJECT_STORAGE) continue ;;
+ /*) ;;
+ *) path="$base/$path" ;;
+ esac
+ # Remove a final symlink itself, not its target. Resolve directory
aliases
+ # before checking them so '..' and symlinked parents cannot hide a
root.
+ while [[ "$path" != / && "$path" == */ ]]; do path="${path%/}"; done
+ if [ -L "$path" ]; then
+ path="$(cd -P "$(dirname "$path")" && pwd -P)/$(basename "$path")"
+ elif [ -d "$path" ]; then
+ path="$(cd -P "$path" && pwd -P)"
+ elif [ -e "$path" ]; then
+ echo "Refusing to remove a non-directory data path: $path"
+ exit 1
+ else
+ continue
+ fi
+ if [ "$path" = / ]; then
+ echo "Refusing to remove the filesystem root."
+ exit 1
+ fi
+ for protected in "$IOTDB_HOME" "$IOTDB_DATA_HOME" "$HOME"; do
+ case "$protected/" in
+ "$path/"*)
+ echo "Refusing to remove a home directory or its parent:
$path"
+ exit 1
+ ;;
+ esac
+ done
+ CLEAN_PATHS+=("$path")
+ done
+}
+
+# The merged process still uses both sets of node directories. Preserve the
+# local destroy-all behavior as well as every custom path from the node tools.
+add_paths "data" "$IOTDB_HOME"
+add_paths "data/datanode" "$IOTDB_DATA_HOME"
+for key in cn_system_dir cn_consensus_dir; do
+ case "$key" in
+ cn_system_dir) fallback=data/confignode/system ;;
+ cn_consensus_dir) fallback=data/confignode/consensus ;;
+ esac
+ value=$(read_property "$CN_CONFIG" "$key" "$fallback")
+ add_paths "$value" "$IOTDB_HOME"
+done
+for key in dn_system_dir dn_data_dirs dn_consensus_dir dn_wal_dirs
dn_tracing_dir dn_sync_dir pipe_receiver_file_dirs
iot_consensus_v2_receiver_file_dirs sort_tmp_dir; do
+ case "$key" in
+ dn_system_dir) fallback=data/datanode/system ;;
+ dn_data_dirs) fallback=data/datanode/data ;;
+ dn_consensus_dir) fallback=data/datanode/consensus ;;
+ dn_wal_dirs) fallback=data/datanode/wal ;;
+ dn_tracing_dir) fallback=datanode/tracing ;;
+ dn_sync_dir) fallback=data/datanode/sync ;;
+ pipe_receiver_file_dirs) fallback=data/datanode/system/pipe/receiver ;;
+ iot_consensus_v2_receiver_file_dirs)
fallback=data/datanode/system/pipe/consensus/receiver ;;
+ sort_tmp_dir) fallback=data/datanode/tmp ;;
+ esac
+ value=$(read_property "$DN_CONFIG" "$key" "$fallback")
+ add_paths "$value" "$IOTDB_DATA_HOME"
+done
+
+# Validate every target before deleting any data, and finish before reporting
success.
+for path in "${CLEAN_PATHS[@]}"; do
+ rm -rf -- "$path"
+done
+echo "IoTDB Edge clean done ..."
diff --git a/scripts/tools/windows/ops/destroy-edge.bat
b/scripts/tools/windows/ops/destroy-edge.bat
new file mode 100644
index 00000000000..417ffda2b2a
--- /dev/null
+++ b/scripts/tools/windows/ops/destroy-edge.bat
@@ -0,0 +1,96 @@
+@REM
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM
+@echo off
+setlocal EnableExtensions DisableDelayedExpansion
+
+if not "%~2"=="" goto usage
+if "%~1"=="-f" goto confirmed
+if not "%~1"=="" goto usage
+set "CLEAN_SERVICE="
+set /p "CLEAN_SERVICE=Do you want to clean all the data of IoTDB Edge? y/n
(default n): "
+if /i "%CLEAN_SERVICE%"=="y" goto confirmed
+echo Exiting...
+exit /b 0
+
+:confirmed
+if not defined IOTDB_HOME set "IOTDB_HOME=%~dp0..\..\.."
+for %%i in ("%IOTDB_HOME%") do set "IOTDB_HOME=%%~fi"
+if not defined IOTDB_CONF set "IOTDB_CONF=%IOTDB_HOME%\conf"
+if not exist "%IOTDB_HOME%\sbin\windows\stop-edge.bat" (
+ echo Cannot find the Edge stop script. No data has been removed.
+ exit /b 1
+)
+
+@REM Wait for the merged process and propagate stop errors before removing
data.
+call "%IOTDB_HOME%\sbin\windows\stop-edge.bat" -f
+if errorlevel 1 exit /b 1
+
+@REM PowerShell is already required by stop-edge.bat. Literal paths preserve
spaces,
+@REM Unicode, drive/UNC paths and wildcard characters in configured
directories.
+powershell -NoProfile -ExecutionPolicy Bypass -Command ^
+ "$ErrorActionPreference = 'Stop';" ^
+ "$iotdbHome = [IO.Path]::GetFullPath($env:IOTDB_HOME).TrimEnd('\', '/');" ^
+ "$cn = [ordered]@{cn_system_dir='data/confignode/system';
cn_consensus_dir='data/confignode/consensus'};" ^
+ "$dn = [ordered]@{dn_system_dir='data/datanode/system';
dn_data_dirs='data/datanode/data'; dn_consensus_dir='data/datanode/consensus';
dn_wal_dirs='data/datanode/wal'; dn_tracing_dir='datanode/tracing';
dn_sync_dir='data/datanode/sync';
pipe_receiver_file_dirs='data/datanode/system/pipe/receiver';
iot_consensus_v2_receiver_file_dirs='data/datanode/system/pipe/consensus/receiver';
sort_tmp_dir='data/datanode/tmp'};" ^
+ "function Read-Directories($file, $directories) {" ^
+ " $defaults = @{}; foreach ($key in $directories.Keys) {
$defaults[$key] = $directories[$key]; }" ^
+ " if (Test-Path -LiteralPath $file) {" ^
+ " foreach ($line in Get-Content -LiteralPath $file -Encoding UTF8)
{" ^
+ " if ($line -match '^\s*([^#!\s][^=]*?)\s*=\s*(.*?)\s*$') {" ^
+ " $key = $Matches[1].Trim(); $value = $Matches[2].Trim();" ^
+ " if ($directories.Contains($key)) { $directories[$key] =
if ($value) { $value } else { $defaults[$key] }; }" ^
+ " }" ^
+ " }" ^
+ " }" ^
+ "}" ^
+ "$systemConfig = Join-Path $env:IOTDB_CONF 'iotdb-system.properties';" ^
+ "if (Test-Path -LiteralPath $systemConfig) {" ^
+ " Read-Directories $systemConfig $cn; Read-Directories $systemConfig
$dn;" ^
+ "} else {" ^
+ " Read-Directories (Join-Path $env:IOTDB_CONF
'iotdb-confignode.properties') $cn;" ^
+ " Read-Directories (Join-Path $env:IOTDB_CONF
'iotdb-datanode.properties') $dn;" ^
+ "}" ^
+ "$targets = @();" ^
+ "foreach ($directories in (@('data') + @($cn.Values) + @($dn.Values))) {" ^
+ " foreach ($directory in ($directories -split '[,;]')) {" ^
+ " $directory = $directory.Trim();" ^
+ " if (-not $directory -or $directory -match '://' -or $directory
-eq 'OBJECT_STORAGE') { continue; }" ^
+ " if (-not [IO.Path]::IsPathRooted($directory)) { $directory =
Join-Path $iotdbHome $directory; }" ^
+ " $directory = [IO.Path]::GetFullPath($directory).TrimEnd('\',
'/');" ^
+ " $root = [IO.Path]::GetPathRoot($directory).TrimEnd('\', '/');" ^
+ " if ($directory -eq $root) { throw ('Refusing to remove a
filesystem root: ' + $directory); }" ^
+ " foreach ($protected in @($iotdbHome, $env:USERPROFILE)) {" ^
+ " if (-not $protected) { continue; }" ^
+ " $protected = [IO.Path]::GetFullPath($protected).TrimEnd('\',
'/');" ^
+ " if ($protected.Equals($directory,
[StringComparison]::OrdinalIgnoreCase) -or $protected.StartsWith($directory +
'\', [StringComparison]::OrdinalIgnoreCase)) {" ^
+ " throw ('Refusing to remove a home directory or its
parent: ' + $directory);" ^
+ " }" ^
+ " }" ^
+ " $targets += $directory;" ^
+ " }" ^
+ "}" ^
+ "foreach ($directory in ($targets | Select-Object -Unique)) {" ^
+ " if (Test-Path -LiteralPath $directory) { Remove-Item -LiteralPath
$directory -Recurse -Force; }" ^
+ "}" ^
+ "Write-Host 'IoTDB Edge clean done ...';"
+exit /b %errorlevel%
+
+:usage
+echo Usage: %~nx0 [-f]
+exit /b 1