From 465dd29f0149aee1daeb60a3b4e50057d88a08a2 Mon Sep 17 00:00:00 2001 From: JackieTien97 Date: Mon, 31 Aug 2026 17:25:37 +0800 Subject: [PATCH] Add daemon and cleanup tools for IoTDB Edge --- .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(-) create mode 100644 .github/scripts/test-edge-ops.py create mode 100644 scripts/tools/ops/daemon-edge.sh create mode 100644 scripts/tools/ops/destroy-edge.sh create mode 100644 scripts/tools/windows/ops/destroy-edge.bat diff --git a/.github/scripts/test-edge-ops.py b/.github/scripts/test-edge-ops.py new file mode 100644 index 0000000000000..ef16fd04ca84e --- /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 641584170111c..e4adfdd9f0cd9 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 610e53edbfcb3..25662a5fe9570 100644 --- a/distribution/src/assembly/all.xml +++ b/distribution/src/assembly/all.xml @@ -101,6 +101,7 @@ *ainode.* **/*ainode.* + **/*edge.* 0755 diff --git a/distribution/src/assembly/datanode.xml b/distribution/src/assembly/datanode.xml index d3996da97863b..0cb7b66a28093 100644 --- a/distribution/src/assembly/datanode.xml +++ b/distribution/src/assembly/datanode.xml @@ -73,6 +73,7 @@ **/*confignode.* **/*all.* **/*ainode.* + **/*edge.* 0755 diff --git a/distribution/src/assembly/edge.xml b/distribution/src/assembly/edge.xml index 54c598e80b529..67eed08bec33f 100644 --- a/distribution/src/assembly/edge.xml +++ b/distribution/src/assembly/edge.xml @@ -125,6 +125,17 @@ 0755 + + + tools + ${project.basedir}/../scripts/tools + + ops/daemon-edge.sh + ops/destroy-edge.sh + windows/ops/destroy-edge.bat + + 0755 + diff --git a/iotdb-core/datanode/src/assembly/server.xml b/iotdb-core/datanode/src/assembly/server.xml index 2e1dab2e5844d..50a25bbce9e28 100644 --- a/iotdb-core/datanode/src/assembly/server.xml +++ b/iotdb-core/datanode/src/assembly/server.xml @@ -70,6 +70,7 @@ **/*confignode.* **/*all.* **/*ainode.* + **/*edge.* 0755 diff --git a/scripts/sbin/windows/stop-edge.bat b/scripts/sbin/windows/stop-edge.bat index 4eed010be6218..2253f5354c9ab 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 -Force; Write-Host ('IoTDB Edge process ' + $_.ProcessId + ' stopped.') }" -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 { Stop-Process -Id $_.ProcessId -Force; Write-Host ('IoTDB Edge process ' + $_.ProcessId + ' stopped.') }" +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 0000000000000..060ab20319251 --- /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" </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 0000000000000..417ffda2b2a7e --- /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