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 1106d858f10 Add IoTDB Edge distribution: ConfigNode + DataNode merged 
in one JVM (#18538)
1106d858f10 is described below

commit 1106d858f10b567a7f089eae7e518a3c66871c7b
Author: Jackie Tien <[email protected]>
AuthorDate: Mon Aug 31 11:48:52 2026 +0800

    Add IoTDB Edge distribution: ConfigNode + DataNode merged in one JVM 
(#18538)
---
 .github/scripts/test-edge-windows.ps1              | 222 ++++++++++++
 .github/workflows/edge-it.yml                      |  94 +++++
 CLAUDE.md                                          |  13 +
 README.md                                          |  12 +
 README_ZH.md                                       |  13 +
 distribution/pom.xml                               |   2 +
 distribution/src/assembly/all.xml                  |   7 +
 distribution/src/assembly/confignode.xml           |   3 +
 distribution/src/assembly/datanode.xml             |   3 +
 distribution/src/assembly/{all.xml => edge.xml}    |  56 ++-
 .../assembly/resources/conf-edge/logback-edge.xml  | 244 +++++++++++++
 integration-test/pom.xml                           |  16 +
 integration-test/src/assembly/mpp-test.xml         |   3 +
 .../org/apache/iotdb/itbase/category/EdgeIT.java   |  21 ++
 .../org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java | 396 +++++++++++++++++++++
 .../iotdb/confignode/i18n/ConfigNodeMessages.java  |   9 +
 .../iotdb/confignode/i18n/ConfigNodeMessages.java  |   9 +
 .../main/java/org/apache/iotdb/edge/EdgeNode.java  | 122 +++++++
 .../resources/conf/edge/iotdb-system.properties    | 125 +++++++
 scripts/conf/edge-env.sh                           |  69 ++++
 scripts/conf/windows/edge-env.bat                  |  48 +++
 scripts/sbin/start-edge.sh                         |  96 +++++
 scripts/sbin/stop-edge.sh                          | 114 ++++++
 scripts/sbin/windows/check-edge.ps1                |  66 ++++
 scripts/sbin/windows/start-edge.bat                | 109 ++++++
 scripts/sbin/windows/stop-edge.bat                 |  26 ++
 26 files changed, 1884 insertions(+), 14 deletions(-)

diff --git a/.github/scripts/test-edge-windows.ps1 
b/.github/scripts/test-edge-windows.ps1
new file mode 100644
index 00000000000..7ca0f5be06e
--- /dev/null
+++ b/.github/scripts/test-edge-windows.ps1
@@ -0,0 +1,222 @@
+#
+# 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.
+#
+
+$ErrorActionPreference = 'Stop'
+$repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
+$testRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('iotdb-edge-windows-' 
+ [guid]::NewGuid())
+New-Item -ItemType Directory -Path $testRoot | Out-Null
+$javaStub = Join-Path $testRoot 'java.exe'
+$script:caseCount = 0
+$portNames = @(
+    'cn_internal_port', 'cn_consensus_port', 'dn_rpc_port', 'dn_internal_port',
+    'dn_mpp_data_exchange_port', 'dn_schema_region_consensus_port', 
'dn_data_region_consensus_port'
+)
+
+function Get-FreePorts {
+    $result = @{}
+    $reservations = @()
+    try {
+        foreach ($name in $portNames) {
+            $listener = 
[System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
+            $listener.Start()
+            $reservations += $listener
+            $result[$name] = $listener.LocalEndpoint.Port
+        }
+        return $result
+    } finally {
+        foreach ($listener in $reservations) {
+            $listener.Stop()
+        }
+    }
+}
+
+function Invoke-EdgeLauncher {
+    param(
+        [System.Collections.IDictionary]$Ports,
+        [string]$Locale = '',
+        [string[]]$ExtraLines = @(),
+        [switch]$MissingConfig
+    )
+
+    $script:caseCount++
+    $caseDir = Join-Path $testRoot "case-$script:caseCount"
+    $edgeHome = Join-Path $caseDir 'edge installation'
+    $configDir = Join-Path $caseDir 'custom configuration'
+    $launcherDir = Join-Path $edgeHome 'sbin/windows'
+    $envDir = Join-Path $configDir 'windows'
+    $javaHome = Join-Path $caseDir 'fake java'
+    $javaBin = Join-Path $javaHome 'bin'
+    New-Item -ItemType Directory -Path $launcherDir, $envDir, $javaBin -Force 
| Out-Null
+    foreach ($file in @('start-edge.bat', 'check-edge.ps1')) {
+        Copy-Item -LiteralPath (Join-Path $repositoryRoot 
"scripts/sbin/windows/$file") -Destination $launcherDir
+    }
+    Copy-Item -LiteralPath (Join-Path $repositoryRoot 
'scripts/conf/windows/edge-env.bat') -Destination $envDir
+    $common = Get-Content -LiteralPath (Join-Path $repositoryRoot 
'scripts/conf/windows/iotdb-common.bat') -Raw
+    Set-Content -LiteralPath (Join-Path $envDir 'iotdb-common.bat') -Value 
$common.Replace('@tsfile.locale.opt@', $Locale) -Encoding ASCII
+    if (-not $MissingConfig) {
+        $lines = @($ExtraLines)
+        foreach ($key in $Ports.Keys) {
+            $lines += "  $key = $($Ports[$key])  "
+        }
+        Set-Content -LiteralPath (Join-Path $configDir 
'iotdb-system.properties') -Value $lines -Encoding ASCII
+    }
+
+    Copy-Item -LiteralPath $javaStub -Destination $javaBin
+    $argsFile = Join-Path $caseDir 'java-arguments.txt'
+    $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
+    $startInfo.FileName = $env:ComSpec
+    $startInfo.Arguments = '/d /c call "' + (Join-Path $launcherDir 
'start-edge.bat') + '"'
+    $startInfo.WorkingDirectory = $caseDir
+    $startInfo.UseShellExecute = $false
+    $startInfo.CreateNoWindow = $true
+    $startInfo.RedirectStandardOutput = $true
+    $startInfo.RedirectStandardError = $true
+    $startInfo.RedirectStandardInput = $true
+    $startInfo.EnvironmentVariables['IOTDB_HOME'] = $edgeHome
+    $startInfo.EnvironmentVariables['IOTDB_CONF'] = $configDir
+    $startInfo.EnvironmentVariables['JAVA_HOME'] = $javaHome
+    $startInfo.EnvironmentVariables['EDGE_TEST_JAVA_ARGS'] = $argsFile
+    $startInfo.EnvironmentVariables['IOTDB_JMX_OPTS'] = ''
+    $startInfo.EnvironmentVariables['TSFILE_LOCALE_JVM_OPT'] = 
'-Dtsfile.locale=stale'
+    $process = [System.Diagnostics.Process]::new()
+    $process.StartInfo = $startInfo
+    try {
+        [void]$process.Start()
+        $stdout = $process.StandardOutput.ReadToEndAsync()
+        $stderr = $process.StandardError.ReadToEndAsync()
+        $process.StandardInput.Close()
+        if (-not $process.WaitForExit(30000)) {
+            $process.Kill()
+            throw 'Timed out running the Edge Windows launcher'
+        }
+        return [pscustomobject]@{
+            ExitCode = $process.ExitCode
+            Output = $stdout.Result + $stderr.Result
+            JavaInvoked = Test-Path -LiteralPath $argsFile
+            Arguments = if (Test-Path -LiteralPath $argsFile) { Get-Content 
-LiteralPath $argsFile -Raw } else { '' }
+        }
+    } finally {
+        $process.Dispose()
+    }
+}
+
+function Assert-LaunchResult {
+    param($Result, [bool]$ShouldLaunch, [string]$Name)
+    if ($ShouldLaunch) {
+        if ($Result.ExitCode -ne 0 -or -not $Result.JavaInvoked) {
+            throw "${Name}: expected a successful Java launch. 
$($Result.Output)"
+        }
+        if ($Result.Arguments -notmatch 'org\.apache\.iotdb\.edge\.EdgeNode') {
+            throw "${Name}: the Edge main class was not invoked"
+        }
+    } elseif ($Result.ExitCode -eq 0 -or $Result.JavaInvoked) {
+        throw "${Name}: expected rejection before starting Java. 
$($Result.Output)"
+    }
+    Write-Host "PASS: $Name"
+}
+
+try {
+    # Use an executable stub so batch control flow and argument quoting match 
a real JVM.
+    Add-Type -OutputAssembly $javaStub -OutputType ConsoleApplication 
-TypeDefinition @'
+using System;
+using System.IO;
+
+internal static class EdgeJavaStub
+{
+    private static int Main(string[] args)
+    {
+        if (args.Length == 1 && args[0] == "-fullversion")
+        {
+            Console.Error.WriteLine("openjdk full version \"17.0.5+8\"");
+            return 0;
+        }
+        
File.WriteAllLines(Environment.GetEnvironmentVariable("EDGE_TEST_JAVA_ARGS"), 
args);
+        return 0;
+    }
+}
+'@
+
+    $freePorts = Get-FreePorts
+    foreach ($locale in @('', '-Dtsfile.locale=zh')) {
+        $result = Invoke-EdgeLauncher -Ports $freePorts -Locale $locale
+        Assert-LaunchResult $result $true "locale '$locale', custom config and 
paths with spaces"
+        if ($locale -eq '') {
+            if ($result.Arguments -match '-Dtsfile\.locale=') {
+                throw 'The default package inherited a stale TsFile locale 
option'
+            }
+        } elseif ([regex]::Matches($result.Arguments, 
'-Dtsfile\.locale=zh').Count -ne 1 -or $result.Arguments -match 
'tsfile\.locale=stale') {
+            throw 'The zh package did not apply exactly one filtered TsFile 
locale option'
+        }
+    }
+
+    foreach ($name in $portNames) {
+        $listener = 
[System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
+        $listener.Start()
+        try {
+            $ports = $freePorts.Clone()
+            $ports[$name] = $listener.LocalEndpoint.Port
+            $result = Invoke-EdgeLauncher -Ports $ports
+            Assert-LaunchResult $result $false "occupied $name"
+            if ($result.Output -notmatch "The $name $($ports[$name]) is 
already occupied") {
+                throw "The occupied port was not identified correctly: 
$($result.Output)"
+            }
+        } finally {
+            $listener.Stop()
+        }
+    }
+
+    $listener = 
[System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
+    $listener.Start()
+    try {
+        $result = Invoke-EdgeLauncher -Ports $freePorts -ExtraLines @(
+            "# cn_internal_port=$($listener.LocalEndpoint.Port)",
+            "! dn_rpc_port=$($listener.LocalEndpoint.Port)",
+            "cn_internal_port=$($listener.LocalEndpoint.Port)"
+        )
+        Assert-LaunchResult $result $true 'comments, whitespace and the last 
value of a repeated property'
+    } finally {
+        $listener.Stop()
+    }
+
+    $listener = 
[System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 10720)
+    $listener.Start()
+    try {
+        $ports = $freePorts.Clone()
+        $ports.Remove('cn_consensus_port')
+        $result = Invoke-EdgeLauncher -Ports $ports
+        Assert-LaunchResult $result $false 'default port for an omitted 
property'
+        $result = Invoke-EdgeLauncher -Ports @{} -MissingConfig
+        Assert-LaunchResult $result $false 'default ports when the 
configuration file is absent'
+        if ($result.Output -notmatch 'cn_consensus_port 10720 is already 
occupied') {
+            throw 'The missing-file fallback did not check the default 
ConfigNode consensus port'
+        }
+    } finally {
+        $listener.Stop()
+    }
+
+    foreach ($value in @('0', '65536', 'not-a-port')) {
+        $ports = $freePorts.Clone()
+        $ports['dn_rpc_port'] = $value
+        $result = Invoke-EdgeLauncher -Ports $ports
+        Assert-LaunchResult $result $false "invalid port '$value'"
+    }
+    Write-Host "All $script:caseCount Windows Edge launcher cases passed."
+} finally {
+    Remove-Item -LiteralPath $testRoot -Recurse -Force
+}
diff --git a/.github/workflows/edge-it.yml b/.github/workflows/edge-it.yml
new file mode 100644
index 00000000000..64158417011
--- /dev/null
+++ b/.github/workflows/edge-it.yml
@@ -0,0 +1,94 @@
+name: Edge IT
+
+on:
+  push:
+    branches:
+      - master
+      - "rel/*"
+      - "rc/*"
+    paths-ignore:
+      - "docs/**"
+      - "site/**"
+      - "iotdb-client/client-cpp/**"
+      - ".github/workflows/client-cpp-package.yml"
+      - ".github/scripts/package-client-cpp-*.sh"
+      - ".github/workflows/multi-language-client.yml"
+  pull_request:
+    branches:
+      - master
+      - "rel/*"
+      - "rc/*"
+    paths-ignore:
+      - "docs/**"
+      - "site/**"
+      - "iotdb-client/client-cpp/**"
+      - ".github/workflows/client-cpp-package.yml"
+      - ".github/scripts/package-client-cpp-*.sh"
+      - ".github/workflows/multi-language-client.yml"
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+env:
+  MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false 
-Dmaven.wagon.http.retryHandler.class=standard 
-Dmaven.wagon.http.retryHandler.count=3
+  MAVEN_ARGS: --batch-mode --no-transfer-progress
+  DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+
+jobs:
+  Ubuntu:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v5
+      - name: Set up JDK
+        uses: actions/setup-java@v5
+        with:
+          distribution: corretto
+          java-version: 17
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: Cache Maven packages
+        uses: actions/cache@v5
+        with:
+          path: ~/.m2
+          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+          restore-keys: ${{ runner.os }}-m2-
+      - name: Build Edge distribution
+        shell: bash
+        run: |
+          mvn clean package \
+          -DskipTests \
+          -pl distribution \
+          -am
+      - name: Edge IT
+        shell: bash
+        run: |
+          mvn verify \
+          -P with-integration-tests,EdgeIT \
+          -DskipUTs \
+          -Dit.test=IoTDBEdgeBasicIT \
+          -DEdgeConfigNodeAddress=127.0.0.2 \
+          -DfailIfNoTests=false \
+          -Dfailsafe.failIfNoSpecifiedTests=false \
+          -pl integration-test \
+          -am
+      - name: Upload Artifact
+        if: failure()
+        uses: actions/upload-artifact@v6
+        with:
+          name: edge-log-Linux
+          path: |
+            integration-test/target/edge-it/**/*.log
+            integration-test/target/failsafe-reports
+          if-no-files-found: ignore
+          retention-days: 1
+
+  WindowsScripts:
+    runs-on: windows-latest
+    steps:
+      - uses: actions/checkout@v5
+      - name: Test Edge Windows launchers
+        shell: powershell
+        run: .github/scripts/test-edge-windows.ps1
diff --git a/CLAUDE.md b/CLAUDE.md
index c1852372d90..33015b568d9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -92,6 +92,10 @@ mvn clean verify -DskipUTs -Dit.test=ClassName 
-DfailIfNoTests=false -Dfailsafe.
 
 # Run a single test method within an IT class (use ClassName#methodName)
 mvn clean verify -DskipUTs -Dit.test=ClassName#methodName 
-DfailIfNoTests=false -Dfailsafe.failIfNoSpecifiedTests=false -pl 
integration-test -am -PTableSimpleIT -P with-integration-tests
+
+# Build the Edge distribution and run its dedicated Tree/Table read-write IT
+mvn clean package -DskipTests -pl distribution -am
+mvn verify -DskipUTs -Dit.test=IoTDBEdgeBasicIT -DfailIfNoTests=false 
-Dfailsafe.failIfNoSpecifiedTests=false -pl integration-test -am -P EdgeIT -P 
with-integration-tests
 ```
 
 When verifying a new feature, only run the specific IT classes/methods that 
were added or modified in the current branch — do not run all ITs.
@@ -113,6 +117,15 @@ To run integration tests from IntelliJ: enable the 
`with-integration-tests` prof
 - **DataNode** (`iotdb-core/datanode`): Handles data storage, query execution, 
and client connections. The main server component.
 - **AINode** (`iotdb-core/ainode`): Python-based node for AI/ML inference 
tasks.
 
+### Edge Distribution
+
+- `EdgeNode` 
(`iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java`) 
starts the ConfigNode and DataNode in one JVM for resource-constrained 
single-node deployments.
+- `distribution/src/assembly/edge.xml` produces the additional 
`apache-iotdb-<version>-edge-bin.zip` artifact. Keep its `edge-bin` assembly id 
unique so it cannot replace an existing attached artifact.
+- Edge-specific system defaults live under 
`iotdb-core/node-commons/src/assembly/resources/conf/edge/`. Standard node, 
all-in-one, and integration-test assemblies must continue to exclude `edge/**`.
+- Edge launchers must reuse the configuration-aware port checks in 
`scripts/conf/iotdb-common.sh`. Stop scripts must match both the `EdgeNode` 
main class and the exact `IOTDB_HOME`; never fall back to a global process-name 
kill.
+- Package node-independent tools that work with the combined process. Exclude 
scripts that require standalone node launchers or environment files, including 
destructive, daemon-management, and health-check scripts.
+- After packaging changes, run a clean distribution build, check that every 
existing distribution artifact is still produced, and inspect the Edge zip 
contents and startup/shutdown behavior.
+
 ### Dual Data Model
 
 IoTDB supports two data models operating on the same storage:
diff --git a/README.md b/README.md
index 8d6923fbdb2..d444e312d51 100644
--- a/README.md
+++ b/README.md
@@ -303,6 +303,18 @@ Under the root path of iotdb:
 
 After being built, the IoTDB distribution is located at the folder: 
"distribution/target".
 
+### Build IoTDB Edge
+
+The distribution build also produces `apache-iotdb-<version>-edge-bin.zip`. 
IoTDB Edge runs the ConfigNode and DataNode in one JVM for 
resource-constrained, single-node deployments. It is an additional artifact and 
does not replace any existing distribution package.
+
+After extracting the package, configure `conf/iotdb-system.properties`, then 
start or stop the Edge process with:
+
+```bash
+sbin/start-edge.sh
+sbin/stop-edge.sh
+```
+
+On Windows, use `sbin\windows\start-edge.bat` and 
`sbin\windows\stop-edge.bat`. The package retains tools that are compatible 
with the combined Edge process.
 
 ### Only build cli
 
diff --git a/README_ZH.md b/README_ZH.md
index 73ea63cf67d..415bb42d312 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -160,6 +160,19 @@ git checkout rel/x.x
 
 编译完成后, IoTDB 二进制包将生成在: "distribution/target".
 
+### 源码编译 IoTDB Edge
+
+上述发行版构建还会生成 `apache-iotdb-<version>-edge-bin.zip`。IoTDB Edge 在同一个 JVM 中运行 
ConfigNode 和 DataNode,适用于资源受限的单机部署。它是新增制品,不会替换任何已有发行包。
+
+解压后,可在 `conf/iotdb-system.properties` 中调整配置,并使用以下脚本启停 Edge 进程:
+
+```bash
+sbin/start-edge.sh
+sbin/stop-edge.sh
+```
+
+Windows 环境请使用 `sbin\windows\start-edge.bat` 和 
`sbin\windows\stop-edge.bat`。Edge 包会保留与合并进程兼容的工具脚本。
+
 ### 只编译 cli
 
 在 iotdb/iotdb-client 目录下执行:
diff --git a/distribution/pom.xml b/distribution/pom.xml
index 3e618451f58..4a8b1c33c94 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -89,6 +89,7 @@
                                 
<descriptor>src/assembly/confignode.xml</descriptor>
                                 <descriptor>src/assembly/cli.xml</descriptor>
                                 
<descriptor>src/assembly/library-udf.xml</descriptor>
+                                <descriptor>src/assembly/edge.xml</descriptor>
                             </descriptors>
                             
<finalName>apache-iotdb-${project.version}</finalName>
                         </configuration>
@@ -123,6 +124,7 @@
                                         
<include>apache-iotdb-${project.version}-confignode-bin.zip</include>
                                         
<include>apache-iotdb-${project.version}-library-udf-bin.zip</include>
                                         
<include>apache-iotdb-${project.version}-external-service-impl-bin.zip</include>
+                                        
<include>apache-iotdb-${project.version}-edge-bin.zip</include>
                                     </includes>
                                 </fileSet>
                             </fileSets>
diff --git a/distribution/src/assembly/all.xml 
b/distribution/src/assembly/all.xml
index c8b583de664..610e53edbfc 100644
--- a/distribution/src/assembly/all.xml
+++ b/distribution/src/assembly/all.xml
@@ -57,6 +57,9 @@
         <fileSet>
             <outputDirectory>conf</outputDirectory>
             
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+            <excludes>
+                <exclude>edge/**</exclude>
+            </excludes>
         </fileSet>
         <fileSet>
             <outputDirectory>conf</outputDirectory>
@@ -64,6 +67,8 @@
             <excludes>
                 <exclude>ainode-env.*</exclude>
                 <exclude>**/ainode-env.*</exclude>
+                <exclude>edge-env.*</exclude>
+                <exclude>**/edge-env.*</exclude>
                 <exclude>iotdb-common.sh</exclude>
                 <exclude>**/iotdb-common.bat</exclude>
             </excludes>
@@ -85,6 +90,8 @@
             <excludes>
                 <exclude>*ainode.*</exclude>
                 <exclude>**/*ainode.*</exclude>
+                <exclude>*edge.*</exclude>
+                <exclude>**/*edge.*</exclude>
             </excludes>
             <fileMode>0755</fileMode>
         </fileSet>
diff --git a/distribution/src/assembly/confignode.xml 
b/distribution/src/assembly/confignode.xml
index 6c3d04558eb..ee107cf17f5 100644
--- a/distribution/src/assembly/confignode.xml
+++ b/distribution/src/assembly/confignode.xml
@@ -44,6 +44,9 @@
         <fileSet>
             <outputDirectory>conf</outputDirectory>
             
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+            <excludes>
+                <exclude>edge/**</exclude>
+            </excludes>
         </fileSet>
         <fileSet>
             <directory>${project.basedir}/../scripts/conf</directory>
diff --git a/distribution/src/assembly/datanode.xml 
b/distribution/src/assembly/datanode.xml
index 225fa5a7e7d..d3996da9786 100644
--- a/distribution/src/assembly/datanode.xml
+++ b/distribution/src/assembly/datanode.xml
@@ -41,6 +41,9 @@
         <fileSet>
             <outputDirectory>conf</outputDirectory>
             
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+            <excludes>
+                <exclude>edge/**</exclude>
+            </excludes>
         </fileSet>
         <fileSet>
             <directory>${project.basedir}/../scripts/conf</directory>
diff --git a/distribution/src/assembly/all.xml 
b/distribution/src/assembly/edge.xml
similarity index 66%
copy from distribution/src/assembly/all.xml
copy to distribution/src/assembly/edge.xml
index c8b583de664..54c598e80b5 100644
--- a/distribution/src/assembly/all.xml
+++ b/distribution/src/assembly/edge.xml
@@ -20,12 +20,12 @@
 
 -->
 <assembly>
-    <id>all-bin</id>
+    <id>edge-bin</id>
     <formats>
         <format>dir</format>
         <format>zip</format>
     </formats>
-    <baseDirectory>apache-iotdb-${project.version}-all-bin</baseDirectory>
+    <baseDirectory>apache-iotdb-${project.version}-edge-bin</baseDirectory>
     <dependencySets>
         <dependencySet>
             <includes>
@@ -46,27 +46,41 @@
         </dependencySet>
     </dependencySets>
     <fileSets>
+        <!-- jmx credentials and the tool logback come from the datanode 
resources;
+             logback-datanode.xml is replaced by the merged logback-edge.xml 
below. -->
         <fileSet>
             <outputDirectory>conf</outputDirectory>
             
<directory>${project.basedir}/../iotdb-core/datanode/src/assembly/resources/conf</directory>
+            <excludes>
+                <exclude>logback-datanode.xml</exclude>
+            </excludes>
+        </fileSet>
+        <!-- iotdb-system.properties is replaced by the edge-tuned one below. 
-->
+        <fileSet>
+            <outputDirectory>conf</outputDirectory>
+            
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+            <excludes>
+                <exclude>iotdb-system.properties</exclude>
+                <exclude>edge/**</exclude>
+            </excludes>
         </fileSet>
+        <!-- edge-specific system configuration. -->
         <fileSet>
             <outputDirectory>conf</outputDirectory>
-            
<directory>${project.basedir}/../iotdb-core/confignode/src/assembly/resources/conf</directory>
+            
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf/edge</directory>
         </fileSet>
+        <!-- merged logback configuration for the single Edge process. -->
         <fileSet>
             <outputDirectory>conf</outputDirectory>
-            
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+            
<directory>${project.basedir}/src/assembly/resources/conf-edge</directory>
         </fileSet>
         <fileSet>
             <outputDirectory>conf</outputDirectory>
             <directory>${project.basedir}/../scripts/conf</directory>
-            <excludes>
-                <exclude>ainode-env.*</exclude>
-                <exclude>**/ainode-env.*</exclude>
-                <exclude>iotdb-common.sh</exclude>
-                <exclude>**/iotdb-common.bat</exclude>
-            </excludes>
+            <includes>
+                <include>edge-env.sh</include>
+                <include>windows/edge-env.bat</include>
+            </includes>
             <fileMode>0755</fileMode>
         </fileSet>
         <fileSet>
@@ -79,21 +93,35 @@
             <filtered>true</filtered>
             <fileMode>0755</fileMode>
         </fileSet>
+        <!-- one pair of start/stop scripts for the merged process, plus the 
cli. -->
         <fileSet>
             <outputDirectory>sbin</outputDirectory>
             <directory>${project.basedir}/../scripts/sbin</directory>
-            <excludes>
-                <exclude>*ainode.*</exclude>
-                <exclude>**/*ainode.*</exclude>
-            </excludes>
+            <includes>
+                <include>start-edge.sh</include>
+                <include>stop-edge.sh</include>
+                <include>start-cli.sh</include>
+                <include>windows/start-edge.bat</include>
+                <include>windows/check-edge.ps1</include>
+                <include>windows/stop-edge.bat</include>
+                <include>windows/start-cli.bat</include>
+                <include>windows/start-cli-table.bat</include>
+            </includes>
             <fileMode>0755</fileMode>
         </fileSet>
+        <!-- Keep node-independent tools. Edge does not package the standalone 
node launchers or
+             environment files required by the excluded destructive, daemon, 
and health scripts. -->
         <fileSet>
             <outputDirectory>tools</outputDirectory>
             <directory>${project.basedir}/../scripts/tools</directory>
             <excludes>
                 <exclude>*ainode.*</exclude>
                 <exclude>**/*ainode.*</exclude>
+                <exclude>ops/daemon-*.sh</exclude>
+                <exclude>ops/destroy-*.sh</exclude>
+                <exclude>ops/health_check.sh</exclude>
+                <exclude>windows/ops/destroy-*.bat</exclude>
+                <exclude>windows/ops/health_check.bat</exclude>
             </excludes>
             <fileMode>0755</fileMode>
         </fileSet>
diff --git a/distribution/src/assembly/resources/conf-edge/logback-edge.xml 
b/distribution/src/assembly/resources/conf-edge/logback-edge.xml
new file mode 100644
index 00000000000..3d36fbf1406
--- /dev/null
+++ b/distribution/src/assembly/resources/conf-edge/logback-edge.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<configuration scan="true" scanPeriod="60 seconds">
+    <jmxConfigurator/>
+    <!-- prevent logback from outputting its own status at the start of every 
log -->
+    <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="FILEERROR">
+        <file>${IOTDB_HOME}/logs/log_edge_error.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-error-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>error</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="FILEWARN">
+        <file>${IOTDB_HOME}/logs/log_edge_warn.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-warn-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="FILEDEBUG">
+        <file>${IOTDB_HOME}/logs/log_edge_debug.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-debug-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>DEBUG</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="FILETRACE">
+        <file>${IOTDB_HOME}/logs/log_edge_trace.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-trace-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>TRACE</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.ConsoleAppender" name="stdout">
+        <Target>System.out</Target>
+        <encoder>
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>${CONSOLE_LOG_LEVEL:-DEBUG}</level>
+        </filter>
+    </appender>
+    <!-- a log appender that collect all log records whose level is greater 
than debug-->
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="FILEALL">
+        <file>${IOTDB_HOME}/logs/log_edge_all.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-all-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="FILE_COST_MEASURE">
+        <file>${IOTDB_HOME}/logs/log_edge_measure.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-measure-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="QUERY_DEBUG">
+        <file>${IOTDB_HOME}/logs/log_edge_query_debug.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-query-debug-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="SLOW_SQL">
+        <file>${IOTDB_HOME}/logs/log_edge_slow_sql.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-slow-sql-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="SAMPLED_QUERIES">
+        <file>${IOTDB_HOME}/logs/log_edge_sampled_queries.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-sampled-queries-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="COMPACTION">
+        <file>${IOTDB_HOME}/logs/log_edge_compaction.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-compaction-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" 
name="EXPLAIN_ANALYZE">
+        <file>${IOTDB_HOME}/logs/log_explain_analyze.log</file>
+        <rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            
<fileNamePattern>${IOTDB_HOME}/logs/log-edge-explain-%d{yyyyMMdd}.log.gz</fileNamePattern>
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <append>true</append>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d [%t] %-5p %C{25}:%L - %m %n</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+    <root level="info">
+        <appender-ref ref="FILETRACE"/>
+        <appender-ref ref="FILEDEBUG"/>
+        <appender-ref ref="FILEWARN"/>
+        <appender-ref ref="FILEERROR"/>
+        <appender-ref ref="FILEALL"/>
+        <appender-ref ref="stdout"/>
+    </root>
+    <logger level="OFF" name="io.moquette.broker.metrics.MQTTMessageLogger"/>
+    <logger level="info" name="org.apache.iotdb.db.service"/>
+    <logger level="info" name="org.apache.iotdb.db.conf"/>
+    <logger level="info" name="org.apache.iotdb.db.cost.statistic">
+        <appender-ref ref="FILE_COST_MEASURE"/>
+    </logger>
+    <logger level="info" name="QUERY_DEBUG">
+        <appender-ref ref="QUERY_DEBUG"/>
+    </logger>
+    <logger level="info" name="SLOW_SQL" additivity="false">
+        <appender-ref ref="SLOW_SQL"/>
+    </logger>
+    <logger level="info" name="SAMPLED_QUERIES" additivity="false">
+        <appender-ref ref="SAMPLED_QUERIES"/>
+    </logger>
+    <logger level="info" name="QUERY_FREQUENCY"/>
+    <logger level="info" name="DETAILED_FAILURE_QUERY_TRACE"/>
+    <logger level="info" name="COMPACTION">
+        <appender-ref ref="COMPACTION"/>
+    </logger>
+    <logger level="info" name="org.apache.iotdb.pipe.api"/>
+    <logger level="info" name="org.apache.iotdb.db.pipe"/>
+    <logger level="info" name="org.apache.iotdb.commons.pipe"/>
+    <logger level="info" name="EXPLAIN_ANALYZE" additivity="false">
+        <appender-ref ref="EXPLAIN_ANALYZE"/>
+    </logger>
+</configuration>
diff --git a/integration-test/pom.xml b/integration-test/pom.xml
index 2152a5e1134..f6624fbd65d 100644
--- a/integration-test/pom.xml
+++ b/integration-test/pom.xml
@@ -30,6 +30,7 @@
     <name>IoTDB: Integration-Test</name>
     <properties>
         <integrationTest.excludedGroups/>
+        
<integrationTest.edgePackage>${maven.multiModuleProjectDirectory}/distribution/target/apache-iotdb-${project.version}-edge-bin.zip</integrationTest.edgePackage>
         <integrationTest.forkCount>1</integrationTest.forkCount>
         <integrationTest.includedGroups/>
         
<integrationTest.launchNodeInSameJVM>true</integrationTest.launchNodeInSameJVM>
@@ -333,6 +334,7 @@
                             <forkCount>${integrationTest.forkCount}</forkCount>
                             <reuseForks>false</reuseForks>
                             <systemPropertyVariables>
+                                
<EdgePackage>${integrationTest.edgePackage}</EdgePackage>
                                 <TestEnv>${integrationTest.testEnv}</TestEnv>
                                 
<RandomSelectWriteNode>${integrationTest.randomSelectWriteNode}</RandomSelectWriteNode>
                                 
<ReadAndVerifyWithMultiNode>${integrationTest.readAndVerifyWithMultiNode}</ReadAndVerifyWithMultiNode>
@@ -447,6 +449,20 @@
         </plugins>
     </build>
     <profiles>
+        <profile>
+            <id>EdgeIT</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+            </activation>
+            <properties>
+                
<integrationTest.excludedGroups>org.apache.iotdb.itbase.category.ManualIT</integrationTest.excludedGroups>
+                
<integrationTest.includedGroups>org.apache.iotdb.itbase.category.EdgeIT</integrationTest.includedGroups>
+                
<integrationTest.launchNodeInSameJVM>true</integrationTest.launchNodeInSameJVM>
+                
<integrationTest.randomSelectWriteNode>false</integrationTest.randomSelectWriteNode>
+                
<integrationTest.readAndVerifyWithMultiNode>false</integrationTest.readAndVerifyWithMultiNode>
+                <integrationTest.testEnv>Simple</integrationTest.testEnv>
+            </properties>
+        </profile>
         <profile>
             <id>SimpleIT</id>
             <activation>
diff --git a/integration-test/src/assembly/mpp-test.xml 
b/integration-test/src/assembly/mpp-test.xml
index 58bb8da0e15..e6fa76d31d8 100644
--- a/integration-test/src/assembly/mpp-test.xml
+++ b/integration-test/src/assembly/mpp-test.xml
@@ -37,6 +37,9 @@
         <fileSet>
             <outputDirectory>conf</outputDirectory>
             
<directory>${project.basedir}/../iotdb-core/node-commons/src/assembly/resources/conf</directory>
+            <excludes>
+                <exclude>edge/**</exclude>
+            </excludes>
         </fileSet>
         <fileSet>
             <outputDirectory>conf</outputDirectory>
diff --git 
a/integration-test/src/main/java/org/apache/iotdb/itbase/category/EdgeIT.java 
b/integration-test/src/main/java/org/apache/iotdb/itbase/category/EdgeIT.java
new file mode 100644
index 00000000000..f70516bd111
--- /dev/null
+++ 
b/integration-test/src/main/java/org/apache/iotdb/itbase/category/EdgeIT.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+package org.apache.iotdb.itbase.category;
+
+public interface EdgeIT {}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java 
b/integration-test/src/test/java/org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java
new file mode 100644
index 00000000000..8bfaf3b0ebd
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/edge/it/IoTDBEdgeBasicIT.java
@@ -0,0 +1,396 @@
+/*
+ * 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.
+ */
+
+package org.apache.iotdb.edge.it;
+
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.it.env.cluster.EnvUtils;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.EdgeIT;
+import org.apache.iotdb.jdbc.Config;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(IoTDBTestRunner.class)
+@Category(EdgeIT.class)
+public class IoTDBEdgeBasicIT {
+
+  private static final Path WORK_DIR =
+      Paths.get("target", "edge-it", 
IoTDBEdgeBasicIT.class.getSimpleName()).toAbsolutePath();
+  private static final Path START_SCRIPT_LOG = 
WORK_DIR.resolve("start-edge-script.log");
+  private static final Path STOP_SCRIPT_LOG = 
WORK_DIR.resolve("stop-edge-script.log");
+
+  private static final long SCRIPT_TIMEOUT_SECONDS = 45;
+  private static final long STARTUP_TIMEOUT_SECONDS = 120;
+  private static final Properties PACKAGED_SYSTEM_PROPERTIES = new 
Properties();
+
+  private static Path edgeHome;
+  private static int[] ports;
+  private static int rpcPort;
+  private static long edgePid = -1;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    deleteRecursively(WORK_DIR);
+    Files.createDirectories(WORK_DIR);
+
+    final String packageProperty = System.getProperty("EdgePackage");
+    assertTrue(
+        "The EdgePackage system property must point to the Edge zip", 
packageProperty != null);
+    final Path edgePackage = 
Paths.get(packageProperty).toAbsolutePath().normalize();
+    assertTrue("Edge package does not exist: " + edgePackage, 
Files.isRegularFile(edgePackage));
+
+    final Path extractionDir = WORK_DIR.resolve("package");
+    unzip(edgePackage, extractionDir);
+    edgeHome = findEdgeHome(extractionDir);
+    try (InputStream input =
+        
Files.newInputStream(edgeHome.resolve("conf/iotdb-system.properties"))) {
+      PACKAGED_SYSTEM_PROPERTIES.load(input);
+    }
+
+    ports = EnvUtils.searchAvailablePorts();
+    rpcPort = ports[2];
+    configurePorts(edgeHome.resolve("conf/iotdb-system.properties"));
+
+    runScript(edgeHome.resolve("sbin/start-edge.sh"), START_SCRIPT_LOG);
+    edgePid = 
Long.parseLong(Files.readString(edgeHome.resolve("edge.pid")).trim());
+    waitUntilReady();
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    AssertionError stopFailure = null;
+    try {
+      if (edgeHome != null && 
Files.isRegularFile(edgeHome.resolve("sbin/stop-edge.sh"))) {
+        try {
+          runScript(edgeHome.resolve("sbin/stop-edge.sh"), STOP_SCRIPT_LOG);
+        } catch (Exception | AssertionError e) {
+          stopFailure = new AssertionError("Failed to stop IoTDB Edge with 
stop-edge.sh", e);
+        }
+      }
+    } finally {
+      stopProcessForciblyIfNeeded();
+      if (ports != null) {
+        Files.deleteIfExists(Paths.get(EnvUtils.getLockFilePath(ports[0])));
+      }
+    }
+    if (stopFailure != null) {
+      throw stopFailure;
+    }
+  }
+
+  @Test
+  public void testTreeModelReadWrite() throws SQLException {
+    try (Connection connection = openTreeConnection();
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE root.edge_it");
+      statement.execute(
+          "CREATE TIMESERIES root.edge_it.device.s1 WITH DATATYPE=INT32, 
ENCODING=PLAIN");
+      statement.execute("INSERT INTO root.edge_it.device(time,s1) VALUES 
(1,42), (2,84)");
+
+      try (ResultSet resultSet =
+          statement.executeQuery("SELECT s1 FROM root.edge_it.device ORDER BY 
TIME")) {
+        assertTrue(resultSet.next());
+        assertEquals(1, resultSet.getLong(1));
+        assertEquals(42, resultSet.getInt(2));
+        assertTrue(resultSet.next());
+        assertEquals(2, resultSet.getLong(1));
+        assertEquals(84, resultSet.getInt(2));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  @Test
+  public void testTableModelReadWrite() throws SQLException {
+    try (Connection connection = openTableConnection();
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE edge_it_table");
+      statement.execute("USE edge_it_table");
+      statement.execute("CREATE TABLE sensor(device STRING TAG, value INT32 
FIELD)");
+      statement.execute("INSERT INTO sensor(time,device,value) VALUES 
(1,'d1',42), (2,'d2',84)");
+
+      try (ResultSet resultSet =
+          statement.executeQuery("SELECT device, value FROM sensor ORDER BY 
time")) {
+        assertTrue(resultSet.next());
+        assertEquals("d1", resultSet.getString(1));
+        assertEquals(42, resultSet.getInt(2));
+        assertTrue(resultSet.next());
+        assertEquals("d2", resultSet.getString(1));
+        assertEquals(84, resultSet.getInt(2));
+        assertFalse(resultSet.next());
+      }
+    }
+  }
+
+  private static Connection openTreeConnection() throws SQLException {
+    return DriverManager.getConnection(
+        jdbcUrl(), SessionConfig.DEFAULT_USER, SessionConfig.DEFAULT_PASSWORD);
+  }
+
+  private static Connection openTableConnection() throws SQLException {
+    return DriverManager.getConnection(
+        jdbcUrl() + "?sql_dialect=table",
+        SessionConfig.DEFAULT_USER,
+        SessionConfig.DEFAULT_PASSWORD);
+  }
+
+  @Test
+  public void testPackagedConfiguration() throws Exception {
+    
assertFalse(PACKAGED_SYSTEM_PROPERTIES.containsKey("model_inference_execution_thread_count"));
+    
assertTrue(Files.isRegularFile(edgeHome.resolve("sbin/windows/check-edge.ps1")));
+
+    final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+    factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+    factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+    factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+    final Document document;
+    try (InputStream input = 
Files.newInputStream(edgeHome.resolve("conf/logback-edge.xml"))) {
+      document = factory.newDocumentBuilder().parse(input);
+    }
+    final Set<String> appenderNames = new HashSet<>();
+    final NodeList appenders = document.getElementsByTagName("appender");
+    for (int i = 0; i < appenders.getLength(); i++) {
+      appenderNames.add(((Element) appenders.item(i)).getAttribute("name"));
+    }
+    final NodeList references = document.getElementsByTagName("appender-ref");
+    for (int i = 0; i < references.getLength(); i++) {
+      final String name = ((Element) references.item(i)).getAttribute("ref");
+      assertTrue("Undefined Edge log appender: " + name, 
appenderNames.contains(name));
+    }
+  }
+
+  private static String jdbcUrl() {
+    return Config.IOTDB_URL_PREFIX + "127.0.0.1:" + rpcPort;
+  }
+
+  private static void configurePorts(final Path configFile) throws IOException 
{
+    final String configNodeAddress = 
System.getProperty("EdgeConfigNodeAddress", "127.0.0.1");
+    final Map<String, String> replacements = new LinkedHashMap<>();
+    replacements.put("cn_seed_config_node", configNodeAddress + ":" + 
ports[0]);
+    replacements.put("dn_seed_config_node", configNodeAddress + ":" + 
ports[0]);
+    replacements.put("cn_internal_address", configNodeAddress);
+    replacements.put("cn_internal_port", Integer.toString(ports[0]));
+    replacements.put("cn_consensus_port", Integer.toString(ports[1]));
+    replacements.put("dn_rpc_address", "127.0.0.1");
+    replacements.put("dn_rpc_port", Integer.toString(ports[2]));
+    replacements.put("dn_internal_address", "127.0.0.1");
+    replacements.put("dn_internal_port", Integer.toString(ports[3]));
+    replacements.put("dn_mpp_data_exchange_port", Integer.toString(ports[4]));
+    replacements.put("dn_schema_region_consensus_port", 
Integer.toString(ports[5]));
+    replacements.put("dn_data_region_consensus_port", 
Integer.toString(ports[6]));
+    replacements.put("cn_metric_prometheus_reporter_port", 
Integer.toString(ports[7]));
+    replacements.put("dn_metric_prometheus_reporter_port", 
Integer.toString(ports[8]));
+
+    final Set<String> replacedKeys = new HashSet<>();
+    final List<String> configuredLines = new ArrayList<>();
+    for (final String line : Files.readAllLines(configFile, 
StandardCharsets.UTF_8)) {
+      final int separatorIndex = line.indexOf('=');
+      final String key = separatorIndex < 0 ? line : line.substring(0, 
separatorIndex).trim();
+      if (replacements.containsKey(key)) {
+        configuredLines.add(key + "=" + replacements.get(key));
+        replacedKeys.add(key);
+      } else {
+        configuredLines.add(line);
+      }
+    }
+    if (!replacedKeys.equals(replacements.keySet())) {
+      final Set<String> missingKeys = new HashSet<>(replacements.keySet());
+      missingKeys.removeAll(replacedKeys);
+      throw new IOException("Missing Edge configuration properties: " + 
missingKeys);
+    }
+    Files.write(configFile, configuredLines, StandardCharsets.UTF_8);
+  }
+
+  private static void waitUntilReady() throws Exception {
+    Class.forName("org.apache.iotdb.jdbc.IoTDBDriver");
+    final long deadline = System.nanoTime() + 
TimeUnit.SECONDS.toNanos(STARTUP_TIMEOUT_SECONDS);
+    SQLException lastException = null;
+    while (System.nanoTime() < deadline) {
+      if 
(!ProcessHandle.of(edgePid).map(ProcessHandle::isAlive).orElse(false)) {
+        break;
+      }
+      try (Connection connection = openTreeConnection();
+          Statement statement = connection.createStatement();
+          ResultSet ignored = statement.executeQuery("SHOW DATABASES")) {
+        return;
+      } catch (SQLException e) {
+        lastException = e;
+      }
+      Thread.sleep(1000);
+    }
+
+    final Path consoleLog = edgeHome.resolve("logs/log_edge_console.log");
+    throw new AssertionError(
+        "IoTDB Edge did not become ready. Last JDBC error: "
+            + lastException
+            + System.lineSeparator()
+            + readLogTail(consoleLog));
+  }
+
+  private static void runScript(final Path script, final Path outputFile) 
throws Exception {
+    Files.createDirectories(outputFile.getParent());
+    final ProcessBuilder processBuilder = new ProcessBuilder("bash", 
script.toString());
+    processBuilder.directory(edgeHome.toFile());
+    processBuilder.redirectErrorStream(true);
+    
processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(outputFile.toFile()));
+    processBuilder.environment().put("IOTDB_HOME", edgeHome.toString());
+    processBuilder.environment().put("IOTDB_CONF", 
edgeHome.resolve("conf").toString());
+    processBuilder.environment().put("IOTDB_DATA_HOME", edgeHome.toString());
+    processBuilder.environment().put("IOTDB_LOG_DIR", 
edgeHome.resolve("logs").toString());
+
+    final Process process = processBuilder.start();
+    if (!process.waitFor(SCRIPT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+      process.destroyForcibly();
+      throw new AssertionError(
+          "Timed out running " + script + System.lineSeparator() + 
readLogTail(outputFile));
+    }
+    if (process.exitValue() != 0) {
+      throw new AssertionError(
+          script
+              + " exited with code "
+              + process.exitValue()
+              + System.lineSeparator()
+              + readLogTail(outputFile));
+    }
+  }
+
+  private static void stopProcessForciblyIfNeeded() throws 
InterruptedException {
+    if (edgePid <= 0) {
+      return;
+    }
+    final ProcessHandle process = ProcessHandle.of(edgePid).orElse(null);
+    if (process == null || !process.isAlive()) {
+      return;
+    }
+    process.destroy();
+    for (int i = 0; i < 10 && process.isAlive(); i++) {
+      Thread.sleep(1000);
+    }
+    if (process.isAlive()) {
+      process.destroyForcibly();
+    }
+  }
+
+  private static Path findEdgeHome(final Path extractionDir) throws 
IOException {
+    try (Stream<Path> paths = Files.list(extractionDir)) {
+      final List<Path> directories = 
paths.filter(Files::isDirectory).collect(Collectors.toList());
+      if (directories.size() != 1) {
+        throw new IOException(
+            "Expected one top-level directory in the Edge package, but found " 
+ directories);
+      }
+      return directories.get(0);
+    }
+  }
+
+  private static void unzip(final Path zipFile, final Path destination) throws 
IOException {
+    Files.createDirectories(destination);
+    try (ZipInputStream input = new 
ZipInputStream(Files.newInputStream(zipFile))) {
+      ZipEntry entry;
+      while ((entry = input.getNextEntry()) != null) {
+        final Path output = destination.resolve(entry.getName()).normalize();
+        if (!output.startsWith(destination)) {
+          throw new IOException("Zip entry escapes the extraction directory: " 
+ entry.getName());
+        }
+        if (entry.isDirectory()) {
+          Files.createDirectories(output);
+        } else {
+          Files.createDirectories(output.getParent());
+          Files.copy(input, output, StandardCopyOption.REPLACE_EXISTING);
+        }
+        input.closeEntry();
+      }
+    }
+  }
+
+  private static String readLogTail(final Path logFile) {
+    if (!Files.isRegularFile(logFile)) {
+      return "Log file does not exist: " + logFile;
+    }
+    try {
+      final List<String> lines = Files.readAllLines(logFile, 
StandardCharsets.UTF_8);
+      return lines.stream()
+          .skip(Math.max(0, lines.size() - 200))
+          .collect(Collectors.joining(System.lineSeparator()));
+    } catch (IOException e) {
+      return "Could not read log file " + logFile + ": " + e;
+    }
+  }
+
+  private static void deleteRecursively(final Path directory) throws 
IOException {
+    if (!Files.exists(directory)) {
+      return;
+    }
+    try (Stream<Path> paths = Files.walk(directory)) {
+      paths
+          .sorted(Comparator.reverseOrder())
+          .forEach(
+              path -> {
+                try {
+                  Files.delete(path);
+                } catch (IOException e) {
+                  throw new UncheckedIOException(e);
+                }
+              });
+    } catch (UncheckedIOException e) {
+      throw e.getCause();
+    }
+  }
+}
diff --git 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
index c4832cd886c..800d59bf727 100644
--- 
a/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
@@ -678,4 +678,13 @@ public final class ConfigNodeMessages {
   public static final String
       
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_SUBSCRIBING_ONLY_TO_THE_AUDIT_DATABASE_OR_PATHS_UNDER_IT_IS_NOT_ALLOWED_3E96A6BA
 =
           "Failed to create or alter topic, subscribing only to the __audit 
database or paths under it is not allowed";
+  public static final String 
LOG_STARTING_IOTDB_EDGE_CONFIGNODE_AND_DATANODE_IN_ONE_77F32605 =
+      "Starting IoTDB Edge: ConfigNode and DataNode in one process";
+  public static final String 
LOG_IOTDB_EDGE_CONFIGNODE_IS_READY_STARTING_DATANODE_6729159E =
+      "IoTDB Edge: ConfigNode is ready, starting DataNode";
+  public static final String 
EXCEPTION_IOTDB_EDGE_CONFIGNODE_BOOTSTRAP_FAILED_02EEE59A =
+      "IoTDB Edge: ConfigNode bootstrap failed";
+  public static final String
+      
EXCEPTION_IOTDB_EDGE_CONFIGNODE_INTERNAL_PORT_ARG_IS_NOT_READY_WITHIN_03697FF5 =
+          "IoTDB Edge: ConfigNode internal port %s is not ready within %s ms";
 }
diff --git 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
index 3029154ab3e..14713cbb801 100644
--- 
a/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
+++ 
b/iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ConfigNodeMessages.java
@@ -721,4 +721,13 @@ public final class ConfigNodeMessages {
   public static final String
       
EXCEPTION_FAILED_TO_CREATE_OR_ALTER_TOPIC_SUBSCRIBING_ONLY_TO_THE_AUDIT_DATABASE_OR_PATHS_UNDER_IT_IS_NOT_ALLOWED_3E96A6BA
 =
           "创建或修改 topic 失败,不允许仅订阅 __audit 数据库或其下的路径";
+  public static final String 
LOG_STARTING_IOTDB_EDGE_CONFIGNODE_AND_DATANODE_IN_ONE_77F32605 =
+      "正在启动 IoTDB Edge:ConfigNode 与 DataNode 运行于同一进程";
+  public static final String 
LOG_IOTDB_EDGE_CONFIGNODE_IS_READY_STARTING_DATANODE_6729159E =
+      "IoTDB Edge:ConfigNode 已就绪,开始启动 DataNode";
+  public static final String 
EXCEPTION_IOTDB_EDGE_CONFIGNODE_BOOTSTRAP_FAILED_02EEE59A =
+      "IoTDB Edge:ConfigNode 启动失败";
+  public static final String
+      
EXCEPTION_IOTDB_EDGE_CONFIGNODE_INTERNAL_PORT_ARG_IS_NOT_READY_WITHIN_03697FF5 =
+          "IoTDB Edge:ConfigNode 内部端口 %s 在 %s ms 内未就绪";
 }
diff --git 
a/iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java 
b/iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java
new file mode 100644
index 00000000000..04396a0be30
--- /dev/null
+++ b/iotdb-core/confignode/src/main/java/org/apache/iotdb/edge/EdgeNode.java
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+
+package org.apache.iotdb.edge;
+
+import org.apache.iotdb.confignode.conf.ConfigNodeDescriptor;
+import org.apache.iotdb.confignode.i18n.ConfigNodeMessages;
+import org.apache.iotdb.confignode.service.ConfigNode;
+import org.apache.iotdb.db.service.DataNode;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Entry point of the IoTDB Edge distribution: starts the ConfigNode and the 
DataNode services
+ * inside ONE JVM process, so that a resource-constrained edge machine only 
pays a single JVM's
+ * fixed overhead (metaspace, code cache, GC structures, thread stacks).
+ *
+ * <p>The ConfigNode is bootstrapped on a background thread first; once its 
internal RPC port
+ * accepts connections (i.e. the seed ConfigNode finished its consensus 
initialization), the
+ * DataNode is started on the main thread. Both services then keep the JVM 
alive with their own
+ * non-daemon threads. If either node fails fatally, its own error handling 
terminates the whole
+ * process, which is the intended single-process semantics of the edge 
deployment.
+ *
+ * <p>Note for launchers: both {@code CONFIGNODE_HOME} and {@code IOTDB_HOME} 
system properties must
+ * point to the installation directory (see {@code sbin/start-edge.sh}), 
otherwise the ConfigNode
+ * resolves its data directories against the process working directory.
+ */
+public final class EdgeNode {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(EdgeNode.class);
+
+  /** Max duration to wait for the ConfigNode internal RPC port to accept 
connections. */
+  private static final long CONFIG_NODE_READY_TIMEOUT_MS = 300_000L;
+
+  private static final long PORT_PROBE_INTERVAL_MS = 500L;
+
+  /** Extra delay after the port opens, leaving time for the leader election 
to settle. */
+  private static final long LEADER_ELECTION_GRACE_MS = 5_000L;
+
+  private EdgeNode() {}
+
+  public static void main(String[] args) throws Exception {
+    
LOGGER.info(ConfigNodeMessages.LOG_STARTING_IOTDB_EDGE_CONFIGNODE_AND_DATANODE_IN_ONE_77F32605);
+
+    final AtomicReference<Throwable> configNodeError = new AtomicReference<>();
+    Thread configNodeThread =
+        new Thread(
+            () -> {
+              try {
+                ConfigNode.main(new String[] {"-s"});
+              } catch (Throwable t) {
+                configNodeError.set(t);
+              }
+            },
+            "EdgeNode-ConfigNode-Bootstrap");
+    configNodeThread.start();
+
+    String internalAddress = 
ConfigNodeDescriptor.getInstance().getConf().getInternalAddress();
+    int internalPort = 
ConfigNodeDescriptor.getInstance().getConf().getInternalPort();
+    waitPortOpen(internalAddress, internalPort, configNodeError);
+    throwIfConfigNodeBootstrapFailed(configNodeError);
+    Thread.sleep(LEADER_ELECTION_GRACE_MS);
+    throwIfConfigNodeBootstrapFailed(configNodeError);
+    
LOGGER.info(ConfigNodeMessages.LOG_IOTDB_EDGE_CONFIGNODE_IS_READY_STARTING_DATANODE_6729159E);
+
+    // DataNode.main returns after a successful start; the services of both 
nodes keep the JVM
+    // alive with non-daemon threads afterwards.
+    DataNode.main(new String[] {"-s"});
+  }
+
+  private static void 
throwIfConfigNodeBootstrapFailed(AtomicReference<Throwable> configNodeError) {
+    Throwable error = configNodeError.get();
+    if (error != null) {
+      throw new IllegalStateException(
+          
ConfigNodeMessages.EXCEPTION_IOTDB_EDGE_CONFIGNODE_BOOTSTRAP_FAILED_02EEE59A, 
error);
+    }
+  }
+
+  private static void waitPortOpen(
+      String address, int port, AtomicReference<Throwable> configNodeError)
+      throws InterruptedException {
+    long deadline = System.currentTimeMillis() + CONFIG_NODE_READY_TIMEOUT_MS;
+    while (System.currentTimeMillis() < deadline) {
+      if (configNodeError.get() != null) {
+        return;
+      }
+      try (Socket socket = new Socket()) {
+        socket.connect(new InetSocketAddress(address, port), 1000);
+        return;
+      } catch (Exception e) {
+        Thread.sleep(PORT_PROBE_INTERVAL_MS);
+      }
+    }
+    throw new IllegalStateException(
+        String.format(
+            ConfigNodeMessages
+                
.EXCEPTION_IOTDB_EDGE_CONFIGNODE_INTERNAL_PORT_ARG_IS_NOT_READY_WITHIN_03697FF5,
+            port,
+            CONFIG_NODE_READY_TIMEOUT_MS));
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties
 
b/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties
new file mode 100644
index 00000000000..13c87b5295b
--- /dev/null
+++ 
b/iotdb-core/node-commons/src/assembly/resources/conf/edge/iotdb-system.properties
@@ -0,0 +1,125 @@
+#
+# 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.
+#
+
+####################
+### Cluster Configuration
+####################
+
+cluster_name=defaultCluster
+
+####################
+### Seed ConfigNode
+####################
+
+cn_seed_config_node=127.0.0.1:10710
+
+dn_seed_config_node=127.0.0.1:10710
+
+####################
+### Node RPC Configuration
+####################
+
+cn_internal_address=127.0.0.1
+cn_internal_port=10710
+cn_consensus_port=10720
+
+dn_rpc_address=127.0.0.1
+dn_rpc_port=6667
+dn_internal_address=127.0.0.1
+dn_internal_port=10730
+dn_mpp_data_exchange_port=10740
+dn_schema_region_consensus_port=10750
+dn_data_region_consensus_port=10760
+
+####################
+### Replication configuration
+####################
+
+schema_replication_factor=1
+data_replication_factor=1
+
+####################
+### Directory Configuration
+####################
+
+# dn_data_dirs=data/datanode/data
+# dn_wal_dirs=data/datanode/wal
+
+####################
+### Metric Configuration
+####################
+
+# cn_metric_reporter_list=
+cn_metric_prometheus_reporter_port=9091
+
+# dn_metric_reporter_list=
+dn_metric_prometheus_reporter_port=9092
+
+####################
+### IoTDB Edge Tuning
+####################
+# The following defaults are tuned for the edge distribution: ConfigNode and
+# DataNode run in ONE JVM with a small fixed memory budget (see 
conf/edge-env.sh),
+# sharing the machine with other processes. Validated on x86 and Raspberry Pi 
4B.
+
+# ---- thread pools (small fixed sizes instead of CPU-core-based defaults) ----
+query_thread_count=2
+degree_of_query_parallelism=1
+mpp_data_exchange_core_pool_size=2
+mpp_data_exchange_max_pool_size=2
+flush_thread_count=2
+compaction_thread_count=2
+sub_compaction_thread_count=1
+compaction_schedule_thread_num=1
+pipe_subtask_executor_max_thread_num=2
+pipe_sink_selector_number=1
+pipe_sink_max_client_number=8
+continuous_query_submit_thread_count=1
+into_operation_execution_thread_count=1
+procedure_core_worker_thread_count=2
+partition_table_recover_worker_num=2
+max_sub_task_num_for_information_table_scan=1
+load_active_listening_max_thread_num=1
+dn_selector_thread_nums_of_client_manager=1
+cn_selector_thread_nums_of_client_manager=1
+max_allowed_concurrent_queries=100
+
+# ---- memory / file buffers ----
+wal_buffer_size_in_byte=1048576
+group_size_in_byte=4194304
+target_compaction_file_size=134217728
+into_operation_buffer_size_in_byte=8388608
+batch_size=10000
+schema_region_ratis_log_appender_buffer_size_max=4194304
+config_node_ratis_log_appender_buffer_size_max=4194304
+
+# ---- background io politeness (share disks with other processes) ----
+compaction_write_throughput_mb_per_sec=8
+partition_table_recover_max_read_mb_per_sec=5
+
+# ---- single-node region / partition layout ----
+schema_region_group_extension_policy=CUSTOM
+default_schema_region_group_num_per_database=1
+data_region_group_extension_policy=CUSTOM
+default_data_region_group_num_per_database=1
+series_slot_num=1
+
+# ---- metrics off (the internal MetricService is shared by both nodes) ----
+cn_metric_level=OFF
+dn_metric_level=OFF
diff --git a/scripts/conf/edge-env.sh b/scripts/conf/edge-env.sh
new file mode 100644
index 00000000000..627623047d4
--- /dev/null
+++ b/scripts/conf/edge-env.sh
@@ -0,0 +1,69 @@
+#!/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.
+#
+
+# IoTDB Edge runs ConfigNode and DataNode inside ONE JVM with a fixed, small
+# memory budget so that it can share a machine with other processes.
+# The defaults below target a total process RSS of about 512 MB and were
+# validated on x86 servers and Raspberry Pi 4B class devices.
+
+# On-heap memory of the merged process. Example values: '224M', '512M'.
+ON_HEAP_MEMORY="${ON_HEAP_MEMORY:-224M}"
+# Initial heap. Kept small so an idle edge instance stays light.
+INIT_HEAP_MEMORY="${INIT_HEAP_MEMORY:-64M}"
+# Off-heap (direct buffer) memory.
+OFF_HEAP_MEMORY="${OFF_HEAP_MEMORY:-96M}"
+
+if [ "${OFF_HEAP_MEMORY%"G"}" != "$OFF_HEAP_MEMORY" ]; then
+    off_heap_memory_size_in_mb=$(expr ${OFF_HEAP_MEMORY%"G"} \* 1024)
+else
+    off_heap_memory_size_in_mb=$(expr ${OFF_HEAP_MEMORY%"M"})
+fi
+# Max cached buffer size, which equals OFF_HEAP_MEMORY / io threads number 
(200)
+MAX_CACHED_BUFFER_SIZE=$(expr $off_heap_memory_size_in_mb \* 1024 \* 1024 / 
200)
+
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Diotdb.jmx.local=true"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Xms${INIT_HEAP_MEMORY}"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Xmx${ON_HEAP_MEMORY}"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:MaxDirectMemorySize=${OFF_HEAP_MEMORY}"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS 
-Djdk.nio.maxCachedBufferSize=${MAX_CACHED_BUFFER_SIZE}"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+CrashOnOutOfMemoryError"
+# Serial GC has the lowest fixed memory overhead; typical edge write rates 
leave
+# plenty of latency headroom for its pauses.
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+UseSerialGC"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Xss320k"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:MaxMetaspaceSize=160m"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:CompressedClassSpaceSize=40m"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:ReservedCodeCacheSize=64m"
+# Cap the processors the JVM sees, shrinking internal thread pools on big 
hosts.
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:ActiveProcessorCount=2"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+UnlockDiagnosticVMOptions"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+UseCRC32Intrinsics"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:SafepointTimeoutDelay=1000"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -XX:+SafepointTimeout"
+IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS -Dsun.jnu.encoding=UTF-8 -Dfile.encoding=UTF-8"
+
+# Append tsfile locale option populated by Maven at package time
+# (see conf/iotdb-common.sh; empty in default build, "-Dtsfile.locale=zh" 
under with-zh-locale).
+if [ -n "$TSFILE_LOCALE_JVM_OPT" ]; then
+    IOTDB_JMX_OPTS="$IOTDB_JMX_OPTS $TSFILE_LOCALE_JVM_OPT"
+fi
+
+echo "IoTDB Edge on heap memory size = ${ON_HEAP_MEMORY}B, off heap memory 
size = ${OFF_HEAP_MEMORY}B"
+echo "If you want to change this configuration, please check conf/edge-env.sh."
diff --git a/scripts/conf/windows/edge-env.bat 
b/scripts/conf/windows/edge-env.bat
new file mode 100644
index 00000000000..fccaebcef12
--- /dev/null
+++ b/scripts/conf/windows/edge-env.bat
@@ -0,0 +1,48 @@
+@echo off
+@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
+
+@REM IoTDB Edge runs ConfigNode and DataNode inside ONE JVM with a fixed, small
+@REM memory budget (about 512 MB total process RSS by default).
+
+if "%ON_HEAP_MEMORY%"=="" set ON_HEAP_MEMORY=224M
+if "%INIT_HEAP_MEMORY%"=="" set INIT_HEAP_MEMORY=64M
+if "%OFF_HEAP_MEMORY%"=="" set OFF_HEAP_MEMORY=96M
+
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Diotdb.jmx.local=true
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Xms%INIT_HEAP_MEMORY%
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Xmx%ON_HEAP_MEMORY%
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:MaxDirectMemorySize=%OFF_HEAP_MEMORY%
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+CrashOnOutOfMemoryError
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+UseSerialGC
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Xss320k
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:MaxMetaspaceSize=160m
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:CompressedClassSpaceSize=40m
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:ReservedCodeCacheSize=64m
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:ActiveProcessorCount=2
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+UnlockDiagnosticVMOptions
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -XX:+UseCRC32Intrinsics
+set IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% -Dsun.jnu.encoding=UTF-8 
-Dfile.encoding=UTF-8
+
+@REM Load the Maven-filtered locale before expanding its value in a separate 
command.
+if EXIST "%IOTDB_CONF%\windows\iotdb-common.bat" call 
"%IOTDB_CONF%\windows\iotdb-common.bat"
+if DEFINED TSFILE_LOCALE_JVM_OPT set "IOTDB_JMX_OPTS=%IOTDB_JMX_OPTS% 
%TSFILE_LOCALE_JVM_OPT%"
+
+echo IoTDB Edge on heap memory size = %ON_HEAP_MEMORY%B, off heap memory size 
= %OFF_HEAP_MEMORY%B
+echo If you want to change this configuration, please check 
conf\windows\edge-env.bat.
diff --git a/scripts/sbin/start-edge.sh b/scripts/sbin/start-edge.sh
new file mode 100644
index 00000000000..be1a70ea6f7
--- /dev/null
+++ b/scripts/sbin/start-edge.sh
@@ -0,0 +1,96 @@
+#!/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.
+#
+
+# Start IoTDB Edge: ConfigNode + DataNode in one JVM process.
+
+if [ -z "${IOTDB_HOME}" ]; then
+    export IOTDB_HOME="$(cd "$(dirname "$0")"/.. && pwd)"
+fi
+if [ -z "${IOTDB_CONF}" ]; then
+    export IOTDB_CONF=${IOTDB_HOME}/conf
+fi
+export IOTDB_DATA_HOME=${IOTDB_DATA_HOME:-${IOTDB_HOME}}
+export IOTDB_LOG_DIR=${IOTDB_LOG_DIR:-${IOTDB_HOME}/logs}
+mkdir -p "${IOTDB_LOG_DIR}"
+
+source "$(dirname "$0")/../conf/iotdb-common.sh"
+export CONFIGNODE_HOME=${IOTDB_HOME}
+export CONFIGNODE_DATA_HOME=${IOTDB_DATA_HOME}
+export CONFIGNODE_CONF=${IOTDB_CONF}
+export CONFIGNODE_LOG_DIR=${IOTDB_LOG_DIR}
+
+# Reuse the same configuration-aware port checks as the standard launchers.
+checkAllVariables
+checkAllConfigNodeVariables
+checkConfigNodePortUsages
+checkDataNodePortUsages
+
+. "${IOTDB_CONF}/edge-env.sh"
+
+# find java in JAVA_HOME
+if [ -n "$JAVA_HOME" ]; then
+    for java in "$JAVA_HOME"/bin/amd64/java "$JAVA_HOME"/bin/java; do
+        if [ -x "$java" ]; then
+            JAVA="$java"
+            break
+        fi
+    done
+else
+    JAVA=java
+fi
+if [ -z "$JAVA" ]; then
+    echo "Unable to find java executable. Check JAVA_HOME and PATH environment 
variables." > /dev/stderr
+    exit 1
+fi
+
+illegal_access_params=""
+illegal_access_params="$illegal_access_params 
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED"
+illegal_access_params="$illegal_access_params 
--add-opens=java.base/java.lang=ALL-UNNAMED"
+illegal_access_params="$illegal_access_params 
--add-opens=java.base/java.util=ALL-UNNAMED"
+illegal_access_params="$illegal_access_params 
--add-opens=java.base/java.nio=ALL-UNNAMED"
+illegal_access_params="$illegal_access_params 
--add-opens=java.base/java.io=ALL-UNNAMED"
+illegal_access_params="$illegal_access_params 
--add-opens=java.base/java.net=ALL-UNNAMED"
+
+CLASSPATH=""
+for f in "${IOTDB_HOME}"/lib/*.jar; do
+    CLASSPATH=${CLASSPATH}":"$f
+done
+
+iotdb_parms="-Dlogback.configurationFile=${IOTDB_CONF}/logback-edge.xml"
+iotdb_parms="$iotdb_parms -DIOTDB_HOME=${IOTDB_HOME}"
+# CONFIGNODE_HOME must also point to the installation directory, otherwise the
+# ConfigNode part resolves its data directories against the working directory.
+iotdb_parms="$iotdb_parms -DCONFIGNODE_HOME=${IOTDB_HOME}"
+iotdb_parms="$iotdb_parms -DIOTDB_DATA_HOME=${IOTDB_DATA_HOME}"
+iotdb_parms="$iotdb_parms -DTSFILE_HOME=${IOTDB_HOME}"
+iotdb_parms="$iotdb_parms -DIOTDB_CONF=${IOTDB_CONF}"
+iotdb_parms="$iotdb_parms -DCONFIGNODE_CONF=${IOTDB_CONF}"
+iotdb_parms="$iotdb_parms -DTSFILE_CONF=${IOTDB_CONF}"
+iotdb_parms="$iotdb_parms -Dname=iotdb.EdgeNode"
+iotdb_parms="$iotdb_parms -DIOTDB_LOG_DIR=${IOTDB_LOG_DIR}"
+iotdb_parms="$iotdb_parms -DCONFIGNODE_LOG_DIR=${IOTDB_LOG_DIR}"
+iotdb_parms="$iotdb_parms -DOFF_HEAP_MEMORY=${OFF_HEAP_MEMORY}"
+
+classname=org.apache.iotdb.edge.EdgeNode
+
+echo "Starting IoTDB Edge (ConfigNode + DataNode in one process)"
+nohup "$JAVA" $illegal_access_params $iotdb_parms $IOTDB_JMX_OPTS -cp 
"$CLASSPATH" "$classname" -s > "${IOTDB_LOG_DIR}/log_edge_console.log" 2>&1 &
+echo $! > "${IOTDB_HOME}/edge.pid"
+echo "IoTDB Edge started, pid $(cat "${IOTDB_HOME}/edge.pid"), console log: 
${IOTDB_LOG_DIR}/log_edge_console.log"
diff --git a/scripts/sbin/stop-edge.sh b/scripts/sbin/stop-edge.sh
new file mode 100644
index 00000000000..8f8f98ac64b
--- /dev/null
+++ b/scripts/sbin/stop-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.
+#
+
+# Stop IoTDB Edge (the merged ConfigNode + DataNode process).
+
+IOTDB_HOME="$(cd "$(dirname "$0")"/.. && pwd)"
+
+PID_FILE="${IOTDB_HOME}/edge.pid"
+
+is_same_edge_home() {
+    local command_line="$1"
+    case "$command_line" in
+        *"-DIOTDB_HOME=${IOTDB_HOME} "*|*"-DIOTDB_HOME=${IOTDB_HOME}")
+            return 0
+            ;;
+        *)
+            return 1
+            ;;
+    esac
+}
+
+is_edge_process() {
+    local pid="$1"
+    local command_line
+    command_line=$(ps -ww -p "$pid" -o command= 2>/dev/null)
+    [ -n "$command_line" ] || return 1
+    printf '%s\n' "$command_line" | grep -F -- 
"org.apache.iotdb.edge.EdgeNode" >/dev/null || return 1
+    is_same_edge_home "$command_line"
+}
+
+find_edge_processes() {
+    local process_line
+    local pid
+    while IFS= read -r process_line; do
+        printf '%s\n' "$process_line" | grep -F -- 
"org.apache.iotdb.edge.EdgeNode" >/dev/null || continue
+        is_same_edge_home "$process_line" || continue
+        pid=$(printf '%s\n' "$process_line" | awk '{print $1}')
+        printf '%s\n' "$pid"
+    done < <(ps -axww -o pid= -o command= 2>/dev/null)
+}
+
+stop_edge_process() {
+    local pid="$1"
+    if ! is_edge_process "$pid"; then
+        echo "Refusing to stop PID $pid because it is not IoTDB Edge from 
${IOTDB_HOME}."
+        return 1
+    fi
+    if ! kill "$pid" 2>/dev/null; then
+        echo "Failed to stop IoTDB Edge process $pid."
+        return 1
+    fi
+    for i in $(seq 1 30); do
+        kill -0 "$pid" 2>/dev/null || break
+        sleep 1
+    done
+    if kill -0 "$pid" 2>/dev/null; then
+        if ! is_edge_process "$pid"; then
+            echo "Refusing to force-stop PID $pid because it no longer belongs 
to this IoTDB Edge installation."
+            return 1
+        fi
+        kill -9 "$pid" 2>/dev/null
+    fi
+    echo "IoTDB Edge process $pid stopped."
+}
+
+PID=""
+if [ -f "$PID_FILE" ]; then
+    PID=$(cat "$PID_FILE")
+    case "$PID" in
+        ''|*[!0-9]*)
+            echo "Ignoring invalid PID file ${PID_FILE}."
+            PID=""
+            ;;
+    esac
+    if [ -n "$PID" ] && ! is_edge_process "$PID"; then
+        echo "Ignoring stale PID file ${PID_FILE}; PID $PID does not belong to 
this IoTDB Edge installation."
+        PID=""
+    fi
+    rm -f "$PID_FILE"
+fi
+
+if [ -n "$PID" ]; then
+    stop_edge_process "$PID"
+    exit $?
+fi
+
+FOUND=false
+while IFS= read -r PID; do
+    [ -n "$PID" ] || continue
+    FOUND=true
+    stop_edge_process "$PID" || exit 1
+done < <(find_edge_processes)
+
+if [ "$FOUND" = false ]; then
+    echo "No IoTDB Edge process from ${IOTDB_HOME} is running."
+fi
+exit 0
diff --git a/scripts/sbin/windows/check-edge.ps1 
b/scripts/sbin/windows/check-edge.ps1
new file mode 100644
index 00000000000..f6f9037bb54
--- /dev/null
+++ b/scripts/sbin/windows/check-edge.ps1
@@ -0,0 +1,66 @@
+#
+# 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.
+#
+
+param(
+    [Parameter(Mandatory = $true)]
+    [string]$ConfigFile
+)
+
+$ErrorActionPreference = 'Stop'
+
+# Match the ConfigNode and DataNode defaults when a property is absent.
+$ports = [ordered]@{
+    cn_internal_port = 10710
+    cn_consensus_port = 10720
+    dn_rpc_port = 6667
+    dn_internal_port = 10730
+    dn_mpp_data_exchange_port = 10740
+    dn_schema_region_consensus_port = 10750
+    dn_data_region_consensus_port = 10760
+}
+
+if (Test-Path -LiteralPath $ConfigFile -PathType Leaf) {
+    foreach ($line in Get-Content -LiteralPath $ConfigFile) {
+        if ($line -cmatch '^\s*([^#!\s=]+)\s*=\s*(.*?)\s*$' -and $ports.Keys 
-ccontains $Matches[1]) {
+            $name = $Matches[1]
+            $value = $Matches[2]
+            $port = 0
+            if (-not [int]::TryParse($value, [ref]$port) -or $port -lt 1 -or 
$port -gt 65535) {
+                throw "Invalid port for ${name}: $value"
+            }
+            $ports[$name] = $port
+        }
+    }
+} else {
+    Write-Host "Cannot find $ConfigFile; checking the default ports."
+}
+
+Write-Host 'Checking whether the ConfigNode and DataNode ports are already 
occupied...'
+$listeners = 
[System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpListeners()
+$occupied = $false
+foreach ($entry in $ports.GetEnumerator()) {
+    if ($listeners.Port -contains $entry.Value) {
+        Write-Host "The $($entry.Key) $($entry.Value) is already occupied."
+        $occupied = $true
+    }
+}
+if ($occupied) {
+    exit 1
+}
+exit 0
diff --git a/scripts/sbin/windows/start-edge.bat 
b/scripts/sbin/windows/start-edge.bat
new file mode 100644
index 00000000000..af5f7cb5ca2
--- /dev/null
+++ b/scripts/sbin/windows/start-edge.bat
@@ -0,0 +1,109 @@
+@echo off
+@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
+
+setlocal
+
+@REM set cmd format
+powershell -NoProfile -Command "$v=(Get-ItemProperty 
'HKLM:\SOFTWARE\Microsoft\Windows 
NT\CurrentVersion').CurrentMajorVersionNumber; if($v -gt 6) { cmd /c 'chcp 
65001' }"
+
+title IoTDB Edge
+
+echo ````````````````````````
+echo Starting IoTDB Edge (ConfigNode + DataNode in one process)
+echo ````````````````````````
+
+@REM 
-----------------------------------------------------------------------------
+@REM SET JAVA
+if DEFINED JAVA_HOME set "PATH=%JAVA_HOME%\bin;%PATH%"
+set "FULL_VERSION="
+set "MAJOR_VERSION="
+set "MINOR_VERSION="
+
+for /f tokens^=2-5^ delims^=.-_+^" %%j in ('java -fullversion 2^>^&1') do (
+       set "FULL_VERSION=%%j-%%k-%%l-%%m"
+       IF "%%j" == "1" (
+           set "MAJOR_VERSION=%%k"
+           set "MINOR_VERSION=%%l"
+       ) else (
+           set "MAJOR_VERSION=%%j"
+           set "MINOR_VERSION=%%k"
+       )
+)
+
+set JAVA_VERSION=%MAJOR_VERSION%
+
+@REM IoTDB requires JDK 17 or later.
+IF "%JAVA_VERSION%" == "" (
+       echo Failed to determine Java version. IoTDB only supports jdk ^>= 17, 
please check your java installation.
+       exit /b 1
+)
+IF %JAVA_VERSION% LSS 17 (
+       echo IoTDB only supports jdk ^>= 17, please check your java version.
+       exit /b 1
+)
+
+@REM 
-----------------------------------------------------------------------------
+@REM SET DIRS
+pushd "%~dp0..\.."
+if NOT DEFINED IOTDB_HOME set "IOTDB_HOME=%cd%"
+popd
+if NOT DEFINED IOTDB_CONF set "IOTDB_CONF=%IOTDB_HOME%\conf"
+set "IOTDB_LOG_DIR=%IOTDB_HOME%\logs"
+if NOT EXIST "%IOTDB_LOG_DIR%" mkdir "%IOTDB_LOG_DIR%"
+
+@REM Check both nodes' configured ports before starting the merged process.
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0check-edge.ps1" 
-ConfigFile "%IOTDB_CONF%\iotdb-system.properties"
+if ERRORLEVEL 1 exit /b 1
+
+@REM 
-----------------------------------------------------------------------------
+@REM SET JVM OPTIONS
+if EXIST "%IOTDB_CONF%\windows\edge-env.bat" (
+       call "%IOTDB_CONF%\windows\edge-env.bat"
+) else (
+       echo Can't find %IOTDB_CONF%\windows\edge-env.bat
+       exit /b 1
+)
+
+set 
illegal_access_params=--add-opens=java.base/java.util.concurrent=ALL-UNNAMED 
--add-opens=java.base/java.lang=ALL-UNNAMED 
--add-opens=java.base/java.util=ALL-UNNAMED 
--add-opens=java.base/java.nio=ALL-UNNAMED 
--add-opens=java.base/java.io=ALL-UNNAMED 
--add-opens=java.base/java.net=ALL-UNNAMED
+
+set CLASSPATH=%IOTDB_HOME%\lib\*
+set MAIN_CLASS=org.apache.iotdb.edge.EdgeNode
+
+@REM CONFIGNODE_HOME must also point to the installation directory, otherwise 
the
+@REM ConfigNode part resolves its data directories against the working 
directory.
+set iotdb_parms=-Dlogback.configurationFile="%IOTDB_CONF%\logback-edge.xml"
+set iotdb_parms=%iotdb_parms% -DIOTDB_HOME="%IOTDB_HOME%"
+set iotdb_parms=%iotdb_parms% -DCONFIGNODE_HOME="%IOTDB_HOME%"
+set iotdb_parms=%iotdb_parms% -DIOTDB_DATA_HOME="%IOTDB_HOME%"
+set iotdb_parms=%iotdb_parms% -DTSFILE_HOME="%IOTDB_HOME%"
+set iotdb_parms=%iotdb_parms% -DIOTDB_CONF="%IOTDB_CONF%"
+set iotdb_parms=%iotdb_parms% -DCONFIGNODE_CONF="%IOTDB_CONF%"
+set iotdb_parms=%iotdb_parms% -DTSFILE_CONF="%IOTDB_CONF%"
+set iotdb_parms=%iotdb_parms% -Dname=iotdb.EdgeNode
+set iotdb_parms=%iotdb_parms% -DIOTDB_LOG_DIR="%IOTDB_LOG_DIR%"
+set iotdb_parms=%iotdb_parms% -DCONFIGNODE_LOG_DIR="%IOTDB_LOG_DIR%"
+set iotdb_parms=%iotdb_parms% -DOFF_HEAP_MEMORY=%OFF_HEAP_MEMORY%
+
+@REM 
-----------------------------------------------------------------------------
+@REM START
+java %illegal_access_params% %iotdb_parms% %IOTDB_JMX_OPTS% -cp "%CLASSPATH%" 
%MAIN_CLASS% -s
+set "EDGE_EXIT_CODE=%ERRORLEVEL%"
+pause
+exit /b %EDGE_EXIT_CODE%
diff --git a/scripts/sbin/windows/stop-edge.bat 
b/scripts/sbin/windows/stop-edge.bat
new file mode 100644
index 00000000000..4eed010be62
--- /dev/null
+++ b/scripts/sbin/windows/stop-edge.bat
@@ -0,0 +1,26 @@
+@echo off
+@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 Stopping IoTDB Edge (the merged ConfigNode + DataNode process)
+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

Reply via email to