From f9c10ece4e6ff836dbb29cd354b9045083604e5e Mon Sep 17 00:00:00 2001
From: librelois <c@elo.tf>
Date: Thu, 1 Oct 2020 23:34:40 +0200
Subject: [PATCH] [feat] create duniter launcher

---
 .cargo/config                                 |   2 +
 .gitignore                                    |   6 +-
 .gitlab-ci.yml                                |   6 +-
 .gitlab/test/check_indexes.sh                 |   6 +-
 Cargo.lock                                    | 122 ++++++-
 Cargo.toml                                    |  29 ++
 app/modules/daemon.ts                         | 120 +------
 app/modules/ws2p/index.ts                     |   6 +-
 bin/{duniter => duniter_js}                   |   0
 doc/dev/setup_env_dev.md                      |  61 ++--
 doc/use/manual_compilation.md                 |  29 +-
 duniter.sh                                    |  52 ---
 neon/build.sh                                 |  14 -
 neon/native/artifacts.json                    |   2 +-
 package.json                                  |  10 +-
 release/Makefile                              |   6 +-
 release/arch/arm/build-arm.sh                 |   2 +-
 release/arch/linux/build-lin.sh               |   6 +-
 release/docker/duniter.sh                     |  10 +-
 .../extra/completion/duniter_completion.bash  |   2 -
 release/extra/debian/package/DEBIAN/postinst  |   8 +-
 release/extra/debian/package/DEBIAN/prerm     |   1 +
 release/extra/openrc/duniter.initd            |   6 +-
 release/extra/systemd/duniter.service         |   6 +-
 rust-bins/duniter-launcher/src/config.rs      | 270 ++++++++++++++
 rust-bins/duniter-launcher/src/daemon.rs      | 112 ++++++
 .../duniter-launcher/src/duniter_ts_args.rs   | 145 ++++++++
 rust-bins/duniter-launcher/src/main.rs        | 334 ++++++++++++++++++
 rust-bins/duniter-launcher/src/sync.rs        |  85 +++++
 rust-bins/xtask/Cargo.toml                    |  17 +
 rust-bins/xtask/src/main.rs                   | 145 ++++++++
 server.ts                                     |   4 +-
 test/integration/misc/cli.ts                  |  37 --
 test/run.sh                                   |  11 -
 34 files changed, 1342 insertions(+), 330 deletions(-)
 create mode 100644 .cargo/config
 rename bin/{duniter => duniter_js} (100%)
 delete mode 100755 duniter.sh
 create mode 100644 rust-bins/duniter-launcher/src/config.rs
 create mode 100644 rust-bins/duniter-launcher/src/daemon.rs
 create mode 100644 rust-bins/duniter-launcher/src/duniter_ts_args.rs
 create mode 100644 rust-bins/duniter-launcher/src/main.rs
 create mode 100644 rust-bins/duniter-launcher/src/sync.rs
 create mode 100644 rust-bins/xtask/Cargo.toml
 create mode 100644 rust-bins/xtask/src/main.rs
 delete mode 100755 test/run.sh

diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 000000000..35049cbcb
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,2 @@
+[alias]
+xtask = "run --package xtask --"
diff --git a/.gitignore b/.gitignore
index cfedee3dd..4124c0cfe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,14 +46,16 @@ app/**/*.d.ts
 neon/lib/*.d.ts
 test/**/*.d.ts
 
-# files generated by neon tests
-test2.bin.gz
+# files generated by neon
+neon/native/artifacts.json
 
 # rust binaries
+bin/duniter
 neon/native/index.node
 target
 
 # files generated by rust tests
 neon/native/tests/*.txt
 neon/native/tests/wotb-*
+test2.bin.gz
 **/*.wot
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index be1965504..d16501420 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -11,7 +11,7 @@ workflow:
     - changes:
       - .gitlab/**/*
       - app/**/*
-      - bin/duniter
+      - bin/duniter_js
       - neon/**/*
       - releases/**/*
       - rust-libs/**/*
@@ -27,11 +27,13 @@ workflow:
   tags:
     - redshift
   before_script:
+    - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+    - export PATH="$HOME/.cargo/bin:$PATH"
+    - export RUSTFLAGS="-D warnings"
     - export NVM_DIR="$HOME/.nvm"
     - . "$NVM_DIR/nvm.sh"
     - nvm install 10
     - nvm use 10
-    - export RUSTFLAGS="-D warnings"
 
 
 .cached_nvm: &cached_nvm
diff --git a/.gitlab/test/check_indexes.sh b/.gitlab/test/check_indexes.sh
index 7bc868a6f..52f1bfa49 100755
--- a/.gitlab/test/check_indexes.sh
+++ b/.gitlab/test/check_indexes.sh
@@ -18,7 +18,7 @@ checksum_test() {
   local correct_hash=$2
   local db=$3
   echo "Checking $table's checksum..."
-  bin/duniter --mdb ${db} dump table "$table" > "$DUMP_DIR/$table"
+  bin/duniter_js --mdb ${db} dump table "$table" > "$DUMP_DIR/$table"
   result_hash=`sha1sum "$DUMP_DIR/$table" | grep -Po ".* " | grep -Po "[a-f0-9]+"`
 #  rm -f "$DUMP_DIR/$table"
   if [ "$result_hash" == "$correct_hash" ]; then
@@ -33,8 +33,8 @@ sync_data() {
   local db=$1
   local target=$2
   local target_block=$3
-  local reset_data="bin/duniter --mdb ${db} reset all"
-  local sync="bin/duniter --mdb ${db} sync ${target} --nointeractive ${target_block}"
+  local reset_data="bin/duniter_js --mdb ${db} reset all"
+  local sync="bin/duniter_js --mdb ${db} sync ${target} --nointeractive ${target_block}"
   echo "$reset_data"
   ${reset_data}
   echo "$sync"
diff --git a/Cargo.lock b/Cargo.lock
index 5a170c12c..850a71c53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -33,6 +33,24 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
+
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -70,6 +88,12 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
 
+[[package]]
+name = "base64"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+
 [[package]]
 name = "beef"
 version = "0.4.4"
@@ -92,6 +116,17 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
+[[package]]
+name = "blake2b_simd"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "constant_time_eq",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.7.3"
@@ -173,6 +208,12 @@ dependencies = [
  "vec_map",
 ]
 
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
 [[package]]
 name = "crc32fast"
 version = "1.2.0"
@@ -241,6 +282,16 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "697c714f50560202b1f4e2e09cd50a421881c83e9025db75d15f276616f04f40"
 
+[[package]]
+name = "ctrlc"
+version = "3.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0b676fa23f995faf587496dcd1c80fead847ed58d2da52ac1caca9a72790dd2"
+dependencies = [
+ "nix",
+ "winapi",
+]
+
 [[package]]
 name = "daemonize-me"
 version = "0.3.1"
@@ -261,6 +312,26 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "dirs"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
@@ -344,9 +415,15 @@ dependencies = [
 
 [[package]]
 name = "duniter-launcher"
-version = "0.1.0"
+version = "1.9.0-alpha1"
 dependencies = [
+ "anyhow",
+ "ctrlc",
  "daemonize-me",
+ "dirs",
+ "log",
+ "logwatcher",
+ "nix",
  "rusty-hook",
  "structopt",
 ]
@@ -375,7 +452,7 @@ version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f7315898eda74bf7c9e825f9afae1c9f16debb401fdb7d658fb851fb00a6260c"
 dependencies = [
- "base64",
+ "base64 0.11.0",
  "bs58",
  "byteorder",
  "ring",
@@ -391,7 +468,7 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "287b23c4281016278b47b80baf67186ce756dde738070cc7a41297da5e81dec1"
 dependencies = [
- "base64",
+ "base64 0.11.0",
  "bs58",
  "byteorder",
  "cryptoxide",
@@ -563,13 +640,19 @@ checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005"
 
 [[package]]
 name = "log"
-version = "0.4.8"
+version = "0.4.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
+checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
 dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "logwatcher"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d0925aed5b12ed59857f438d25a910cf051dbcd4107907be1e7abf6c44ec903"
+
 [[package]]
 name = "maplit"
 version = "1.0.2"
@@ -881,6 +964,23 @@ dependencies = [
  "num_cpus",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "redox_users"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "rust-argon2",
+]
+
 [[package]]
 name = "regex"
 version = "1.3.7"
@@ -914,6 +1014,18 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rust-argon2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19"
+dependencies = [
+ "base64 0.12.3",
+ "blake2b_simd",
+ "constant_time_eq",
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.16"
diff --git a/Cargo.toml b/Cargo.toml
index 56e722261..5cf345d30 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,35 @@
+[package]
+authors = ["elois <elois@duniter.org>"]
+description = "Duniter launcher."
+edition = "2018"
+keywords = ["duniter", "launcher"]
+license = "AGPL-3.0"
+name = "duniter-launcher"
+repository = "https://git.duniter.org/nodes/typescript/duniter"
+version = "1.9.0-alpha1"
+
+[[bin]]
+bench = false
+path = "rust-bins/duniter-launcher/src/main.rs"
+name = "duniter"
+
+[dependencies]
+anyhow = "1.0.32"
+ctrlc = "3.1.6"
+daemonize-me = "0.3.1"
+dirs = "3.0.1"
+log = "0.4.11"
+logwatcher = "0.1.1"
+nix = "0.17.0"
+structopt = "0.3.18"
+
+[dev-dependencies]
+rusty-hook = "0.11.2"
+
 [workspace]
 members = [
     "neon/native",
+    "rust-bins/xtask",
     "rust-libs/dubp-wot"
 ]
 
diff --git a/app/modules/daemon.ts b/app/modules/daemon.ts
index 7604e6043..ffd19949d 100644
--- a/app/modules/daemon.ts
+++ b/app/modules/daemon.ts
@@ -13,12 +13,8 @@
 
 import { ConfDTO } from "../lib/dto/ConfDTO";
 import { Server } from "../../server";
-import { Directory, RealFS } from "../lib/system/directory";
 import { ExitCodes } from "../lib/common-libs/exit-codes";
 
-const constants = require("../lib/constants");
-const Tail = require("tail").Tail;
-
 module.exports = {
   duniter: {
     cliOptions: [
@@ -48,51 +44,6 @@ module.exports = {
     },
 
     cli: [
-      {
-        name: "start",
-        desc: "Starts Duniter as a daemon (background task).",
-        logs: false,
-        onConfiguredExecute: async (
-          server: Server,
-          conf: ConfDTO,
-          program: any,
-          params: any
-        ) => {
-          await server.checkConfig();
-          const daemon = server.getDaemon("direct_start", "start");
-          await startDaemon(daemon);
-        },
-      },
-      {
-        name: "stop",
-        desc: "Stops Duniter daemon if it is running.",
-        logs: false,
-        onConfiguredExecute: async (
-          server: Server,
-          conf: ConfDTO,
-          program: any,
-          params: any
-        ) => {
-          const daemon = server.getDaemon();
-          await stopDaemon(daemon);
-        },
-      },
-      {
-        name: "restart",
-        desc: "Stops Duniter daemon and restart it.",
-        logs: false,
-        onConfiguredExecute: async (
-          server: Server,
-          conf: ConfDTO,
-          program: any,
-          params: any
-        ) => {
-          await server.checkConfig();
-          const daemon = server.getDaemon("direct_start", "restart");
-          await stopDaemon(daemon);
-          await startDaemon(daemon);
-        },
-      },
       {
         name: "status",
         desc: "Get Duniter daemon status.",
@@ -114,24 +65,6 @@ module.exports = {
           }
         },
       },
-      {
-        name: "logs",
-        desc: "Follow duniter logs.",
-        logs: false,
-        onConfiguredExecute: async (
-          server: Server,
-          conf: ConfDTO,
-          program: any,
-          params: any
-        ) => {
-          printTailAndWatchFile(
-            Directory.INSTANCE_HOMELOG_FILE,
-            constants.NB_INITIAL_LINES_TO_SHOW
-          );
-          // Never ending command
-          return new Promise((res) => null);
-        },
-      },
       {
         name: "direct_start",
         desc: "Start Duniter node with direct output, non-daemonized.",
@@ -142,6 +75,7 @@ module.exports = {
           params: any,
           startServices: any
         ) => {
+          process.title = program.mdb || "duniter_default";
           const logger = server.logger;
 
           logger.info(">> Server starting...");
@@ -171,55 +105,3 @@ function ServerService(server: Server) {
   server.stopService = () => Promise.resolve();
   return server;
 }
-
-function startDaemon(daemon: any) {
-  return new Promise((resolve, reject) =>
-    daemon.start((err: any) => {
-      if (err) return reject(err);
-      resolve();
-    })
-  );
-}
-
-function stopDaemon(daemon: any) {
-  return new Promise((resolve, reject) =>
-    daemon.stop((err: any) => {
-      err && console.error(err);
-      if (err) return reject(err);
-      resolve();
-    })
-  );
-}
-
-async function printTailAndWatchFile(file: any, tailSize: number) {
-  const fs = RealFS();
-  if (await fs.fsExists(file)) {
-    const content = await fs.fsReadFile(file);
-    const lines = content.split("\n");
-    const from = Math.max(0, lines.length - tailSize);
-    const lastLines = lines.slice(from).join("\n");
-    console.log(lastLines);
-  }
-  watchFile(file);
-}
-
-function watchFile(file: any) {
-  const tail = new Tail(file);
-
-  // Specific errors handling
-  process.on("uncaughtException", (err: any) => {
-    if (err.code === "ENOENT") {
-      console.error("EXCEPTION: ", err.message);
-      setTimeout(() => watchFile(file), 1000); // Wait a second
-    }
-  });
-
-  // On new line
-  tail.on("line", function (data: any) {
-    console.log(data);
-  });
-
-  tail.on("error", function (error: any) {
-    console.error("ERROR: ", error);
-  });
-}
diff --git a/app/modules/ws2p/index.ts b/app/modules/ws2p/index.ts
index d46d3d3bb..be8575a57 100644
--- a/app/modules/ws2p/index.ts
+++ b/app/modules/ws2p/index.ts
@@ -64,8 +64,8 @@ export const WS2PDependency = {
       },
       { value: "--ws2p-prefered-rm  <pubkey>", desc: "Remove prefered node." },
       {
-        value: "--ws2p-prefered-only  <pubkey>",
-        desc: "Only connect to prefered node.",
+        value: "--ws2p-prefered-only",
+        desc: "Only connect to prefered nodes.",
       },
       {
         value: "--ws2p-privileged-add <pubkey>",
@@ -73,7 +73,7 @@ export const WS2PDependency = {
       },
       { value: "--ws2p-privileged-rm <pubkey>", desc: "Remove a privileged." },
       {
-        value: "--ws2p-privileged-only <pubkey>",
+        value: "--ws2p-privileged-only",
         desc: "Accept only connections from a privileged node.",
       },
     ],
diff --git a/bin/duniter b/bin/duniter_js
similarity index 100%
rename from bin/duniter
rename to bin/duniter_js
diff --git a/doc/dev/setup_env_dev.md b/doc/dev/setup_env_dev.md
index ba8c5e547..002b58f1d 100644
--- a/doc/dev/setup_env_dev.md
+++ b/doc/dev/setup_env_dev.md
@@ -10,14 +10,12 @@ Authors: elois
 ## In a post-it
 
 ```bash
-nvm use 10
 git clone git@git.duniter.org:nodes/typescript/duniter.git
 cd duniter
-npm install
-bin/duniter start
+cargo xtask build
+./target/release/duniter start
 ```
 
-
 ## Step by step
 
 ### Prerequisites
@@ -26,31 +24,35 @@ To develop on Duniter, there is currently the following requirement:
 
 - A computer with GNU/Linux or Mac as operating system
 - Build essential tools
-- curl
+- wget
 - git (apt-get install git)
-- Nvm
+- Rust
 
 And preferably an IDE that supports [Typescript] and [Rust] well.
 
 #### Install Prerequisites
 
-Nvm:
-
-    wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
+##### Rust
 
-### Build the project
+```bash
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+```
 
-#### Clone de repository
+Them add `~/.cargo/bin` on your PATH.
 
-    git clone https://git.duniter.org/nodes/typescript/duniter
+If you use `bash`:
 
-#### Install and use the correct version of nodejs
+```bash
+echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> .bashrc
+```
 
-Check the required node version in the `package.json` file on line 5.
+### Build the project
 
-If, for example, version 10 is expected, install and select it with the following command:
+#### Clone the repository
 
-    nvm install 10 && nvm use 10
+```bash
+git clone https://git.duniter.org/nodes/typescript/duniter
+```
 
 #### Build the project and play automated tests
 
@@ -58,22 +60,27 @@ If, for example, version 10 is expected, install and select it with the followin
 
 Command to compile :
 
-    npm install
+```bash
+cargo xtask build
+```
+
+The binary to run duniter-server is then found here: `target/release/duniter`
 
 **WARNING**: playing automated tests takes a lot of resources on your machine and those for several minutes, don't do anything else at the same time!
 
 Command to play automated tests:
 
-    npm test
+```bash
+cargo xtask test
+```
 
 If all the tests are successful, the command ends like this:
 
-    ```bash
-      777 passing (40s)
-      19 pending
-
-    Done in 43.80s.
-    ```
+```bash
+  777 passing (40s)
+  19 pending
+Done in 43.80s.
+```
 
 ### Configure your IDE
 
@@ -88,14 +95,16 @@ For the Rust part, I strongly recommend the following plugins:
 - CodeLLDB (identifier: `vadimcn.vscode-lldb`)
 - rust-analyzer (identifier: `matklad.rust-analyzer`)
 
-The recommended ide configuration can be found in `doc/dev/vscode/settings.json`.
+The recommended IDE configuration can be found in `doc/dev/vscode/settings.json`.
 
 ##### Debugger configuration
 
 The recommended debugger configuration can be found in `doc/dev/.vscode/launch.json`.  
 For import it:
 
-    cp doc/dev/vscode/launch.json .vscode/launch.json
+```bash
+cp doc/dev/vscode/launch.json .vscode/launch.json
+```
 
 You can then adapt it according to your preferences for use :)
 
diff --git a/doc/use/manual_compilation.md b/doc/use/manual_compilation.md
index e3be4c9aa..14767b8e3 100644
--- a/doc/use/manual_compilation.md
+++ b/doc/use/manual_compilation.md
@@ -15,7 +15,7 @@ To compile Duniter manually, there is currently the following requirement:
 - Build essential tools
 - curl
 - tar or unzip or git (to download and extract source code)
-- Nvm
+- Rust
 
 ### Get source code
 
@@ -60,44 +60,25 @@ Depend on your distribution:
 
 TODO: If you know how to install build essential tools for other gnu/linux distributions or for mac, you can complete this documentation and submit a merge request.
 
-#### Nvm
+#### Rust
 
-Nvm:
-
-    wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
+    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
 
 ## Build the project
 
 Go to the root of the folder where you extracted the source code (or possibly cloned from git).
 
-### Install and use the correct version of nodejs
-
-Check the required node version in the `package.json` file on line 5.
-
-If, for example, version 10 is expected, install and select it with the following command:
-
-    nvm install 10 && nvm use 10
-
-### Optionally add GUI
-
-The graphical user interface (GUI) is optional because it is possible to do everything from the command line.  
-If you wish to have the GUI, you must add it (before compiling) with the following command:
-
-    npm add duniter-ui
-
-### Compile
-
 **WARNING**: the compilation of the project requires a lot of resources on your machine, and several long minutes, don't do anything else at the same time!
 
 Command to compile :
 
-    NEON_BUILD_RELEASE=true npm install && npm prune --production
+    cargo xtask build --production
 
 ### Set autocompletion
 
 To install or update Duniter's command auto-completion:
 
-    cp release/extra/completion/duniter_completion.bash /etc/bash_completion.d/
+    bin/duniter completions bash > /etc/bash_completion.d/duniter_completion.bash
 
 ### Run on command line
 
diff --git a/duniter.sh b/duniter.sh
deleted file mode 100755
index 9fd048d49..000000000
--- a/duniter.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/bin/bash
-
-##########################
-#    DUNITER EXECUTABLE
-#
-# Wraps bin/duniter that is called with Node.js
-#
-
-DEB_PACKAGING=
-
-if [[ $DEB_PACKAGING ]]; then
-  DUNITER_DIR=/opt/duniter/
-fi
-
-duniter() {
-
-	local NODE
-	local LOGS_FILE
-
-	if [ -z "$DEV_MODE" ]; then
-
-		### Production mode
-		if [[ -d $DUNITER_DIR/node ]]; then
-			NODE=$DUNITER_DIR/node/bin/node
-	  else
-	    echo "Node.js is not embedded in this version of Duniter"
-	    return
-		fi;
-	else
-
-		### Cheating with DEV mode
-		DUNITER_DIR=`pwd`
-		NODE=node
-	fi
-
-	VERSION=`$NODE -v`
-
-	if [[ $VERSION != v10* ]]; then
-	  echo "$NODE v10 is required";
-	else
-
-	  # Calls duniter JS command
-	  cd $DUNITER_DIR
-	  $NODE "$DUNITER_DIR/bin/duniter" "$@"
-
-	fi;
-}
-
-# If the script was launched with parameters, try to launch the Duniter command
-if [ ! -z $1 ]; then
-	duniter "$@"
-fi
diff --git a/neon/build.sh b/neon/build.sh
index a71a80952..c14123d47 100755
--- a/neon/build.sh
+++ b/neon/build.sh
@@ -1,19 +1,5 @@
 #!/bin/sh
 
-if [ -z "${DUNITER_FAST_BUILD}" ]; then
-    if [ "$(command -v rustup)" ]; then
-        rustup update stable
-    elif [ ! "$(command -v cargo)" ]; then
-        curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
-        export PATH="$HOME/.cargo/bin:$PATH"
-    fi
-else
-    echo "WARNING: you have disabled the automatic update of Rust, remember to update Rust regularly with command \"rustup update\"."
-fi
-
-rustc --version
-cargo --version
-
 cd neon
 
 if [ "${NEON_BUILD_RELEASE}" = "true" ] || [ "${NODE_ENV}" = "production" ]; then
diff --git a/neon/native/artifacts.json b/neon/native/artifacts.json
index 837b6fb09..5c5d995cc 100644
--- a/neon/native/artifacts.json
+++ b/neon/native/artifacts.json
@@ -1 +1 @@
-{"active":"debug","targets":{"debug":{"rustc":"","env":{"npm_config_target":null,"npm_config_arch":null,"npm_config_target_arch":null,"npm_config_disturl":null,"npm_config_runtime":null,"npm_config_build_from_source":null,"npm_config_devdir":null}},"release":{"rustc":"","env":{"npm_config_target":null,"npm_config_arch":null,"npm_config_target_arch":null,"npm_config_disturl":null,"npm_config_runtime":null,"npm_config_build_from_source":null,"npm_config_devdir":null}}}}
\ No newline at end of file
+{"active":"debug","targets":{"debug":{"rustc":"","env":{"npm_config_target":null,"npm_config_arch":null,"npm_config_target_arch":null,"npm_config_disturl":null,"npm_config_runtime":null,"npm_config_build_from_source":null,"npm_config_devdir":null}}}}
\ No newline at end of file
diff --git a/package.json b/package.json
index f42fe4445..31b742349 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
   "private": false,
   "description": "Crypto-currency software allowing to build P2P free currencies",
   "main": "index.js",
-  "node-main": "./bin/duniter",
+  "node-main": "./bin/duniter_js",
   "window": {
     "icon": "duniter.png",
     "title": "v1.8.1",
@@ -28,10 +28,8 @@
     "tsc": "tsc",
     "tscw": "tsc -w",
     "doc": "typedoc --out typedoc/ index.ts app/ --mode file --readme README.md --includeDeclarations --sourcefile-url-prefix \"https://git.duniter.org/nodes/typescript/duniter/blob/loki/\"",
-    "test": "./test/run.sh",
-    "test:rs": "./test/run.sh rs",
-    "test:ts": "./test/run.sh ts",
-    "start": "node bin/duniter start",
+    "test": "nyc --reporter html mocha",
+    "start": "cargo run -- start",
     "build": "./neon/build.sh && cd.. && tsc && cd \"../node_modules/duniter-ui\" && npm install && npm run build",
     "install": "./neon/build.sh",
     "lint": "lint-staged",
@@ -138,7 +136,7 @@
   },
   "peerDependencies": {},
   "bin": {
-    "duniter": "./bin/duniter"
+    "duniter": "./bin/duniter_js"
   },
   "lint-staged": {
     "app/**/*.{js,jsx,ts,tsx,md,html,css,scss}": [
diff --git a/release/Makefile b/release/Makefile
index 6d32bbb7e..3b9617d92 100644
--- a/release/Makefile
+++ b/release/Makefile
@@ -85,6 +85,8 @@ base-gui: $(call nodedep,duniter-ui) base
 # Build the base
 .PHONY: base
 base: | node_modules
+	@cargo build --release -p duniter-launcher
+	@mv target/release/duniter bin/duniter
 
 index.html: $(call nodedep,duniter-ui) $(addprefix node_modules/,$(leveldown.COMPILED) $(sqlite3.COMPILED))
 	@sed -i "s/\"main\": \"index.js\",/\"main\": \"index.html\",/" package.json
@@ -121,10 +123,12 @@ endif
 	@rm -rf $(DEV_FILES)
 	@mv release/extra .
 	@rm -rf coverage coverage.* release test
-	@rm -rf target
 	@rm -rf neon/native/target
 	@rm -rf node_modules/sqlite3/build
 	@rm -rf node_modules/duniter-ui/node_modules
+	@rm -rf rust-bins
+	@rm -rf rust-libs
+	@rm -rf target
 
 ######
 # We are on main project
diff --git a/release/arch/arm/build-arm.sh b/release/arch/arm/build-arm.sh
index c7b903b6a..0c28b41fa 100755
--- a/release/arch/arm/build-arm.sh
+++ b/release/arch/arm/build-arm.sh
@@ -64,7 +64,7 @@ mkdir -p "$RELEASES"
 
 cd "$DOWNLOADS/duniter"
 echo "Build Duniter server with GUI..."
-make -C release server-gui clean
+make -C release ADD_DEBUG=N server-gui clean
 mv "$DOWNLOADS/duniter/work" "$RELEASES/duniter"
 cd ${RELEASES}/duniter
 echo "Copying Nodejs"
diff --git a/release/arch/linux/build-lin.sh b/release/arch/linux/build-lin.sh
index d0487395e..d72647d69 100755
--- a/release/arch/linux/build-lin.sh
+++ b/release/arch/linux/build-lin.sh
@@ -116,11 +116,11 @@ cp -r ~/.nvm/versions/node/${NVER}/ node-${NVER}-linux-x64
 # -----------
 
 pushd "${ROOT}"
-make -C release DEST="${RELEASES_SUBDIR}/duniter" base-gui || exit 1
+make -C release ADD_DEBUG=N DEST="${RELEASES_SUBDIR}/duniter" base-gui || exit 1
 cp -pr "${RELEASES}/duniter" "${RELEASES}/desktop_" || exit 1
-make -C release DEST="${RELEASES_SUBDIR}/desktop_" desktop clean || exit 1
+make -C release ADD_DEBUG=N DEST="${RELEASES_SUBDIR}/desktop_" desktop clean || exit 1
 cp -pr "${RELEASES}/duniter" "${RELEASES}/server_" || exit 1
-make -C release DEST="${RELEASES_SUBDIR}/server_" server-gui clean || exit 1
+make -C release ADD_DEBUG=N DEST="${RELEASES_SUBDIR}/server_" server-gui clean || exit 1
 popd
 
 # --------------------------------
diff --git a/release/docker/duniter.sh b/release/docker/duniter.sh
index 270d084bb..e27a0d153 100644
--- a/release/docker/duniter.sh
+++ b/release/docker/duniter.sh
@@ -6,16 +6,14 @@ if [[ -z ${1} ]]; then
 fi
 
 # Options
-DUNITER_OPTS=
-DUNITER_OPTS="${DUNITER_OPTS} --webmhost 0.0.0.0"
-DUNITER_OPTS="${DUNITER_OPTS} --home /var/lib/duniter"
-DUNITER_OPTS="${DUNITER_OPTS} --mdb duniter_default"
+export DUNITER_WEB_UI_HOST = "0.0.0.0"
+
 
 # Key file found
 if [[ -f /etc/duniter/key.yml ]]; then
-	DUNITER_OPTS="${DUNITER_OPTS} --keyfile /etc/duniter/keys.yml"
+	export DUNITER_KEYFILE="/etc/duniter/keys.yml"
 fi
 
 # Start duniter
 cd /duniter/duniter/
-bin/duniter ${DUNITER_OPTS} "$@"
+bin/duniter --home /var/lib/duniter "$@"
diff --git a/release/extra/completion/duniter_completion.bash b/release/extra/completion/duniter_completion.bash
index 9333250df..d6572f2af 100644
--- a/release/extra/completion/duniter_completion.bash
+++ b/release/extra/completion/duniter_completion.bash
@@ -85,8 +85,6 @@ direct_webstart \
 --submit-host \
 --submit-port \
 --at \
---salt \
---passwd \
 --keyN \
 --keyr \
 --keyp \
diff --git a/release/extra/debian/package/DEBIAN/postinst b/release/extra/debian/package/DEBIAN/postinst
index 0880595b7..930075ae9 100755
--- a/release/extra/debian/package/DEBIAN/postinst
+++ b/release/extra/debian/package/DEBIAN/postinst
@@ -13,7 +13,6 @@ fi
 # Duniter-Desktop
 if [[ -f $DUN_SOURCES/duniter-desktop ]]; then
   ln -s $DUN_SOURCES/duniter-desktop /usr/bin/duniter-desktop
-  sed -i "s/DEB_PACKAGING=.*/DEB_PACKAGING=true/g" $DUN_SOURCES/duniter.sh
   # Links for Node + NPM
   cd $DUN_SOURCES
   cd bin
@@ -26,9 +25,10 @@ fi
 # Duniter CLI executes with embedded node
 if [[ -d $DUN_SOURCES/node ]]; then
   chmod 755 $DUN_SOURCES/bin/duniter
-  sed -i "s/usr\/bin\/env node/opt\/duniter\/node\/bin\/node/g" $DUN_SOURCES/bin/duniter
-  sed -i "s/DEB_PACKAGING=.*/DEB_PACKAGING=true/g" $DUN_SOURCES/duniter.sh
-  ln -s $DUN_SOURCES/duniter.sh /usr/bin/duniter -f
+  chmod 755 $DUN_SOURCES/bin/duniter_js
+  sed -i "s/usr\/bin\/env node/opt\/duniter\/node\/bin\/node/g" $DUN_SOURCES/bin/duniter_js
+  ln -s $DUN_SOURCES/bin/duniter /usr/bin/duniter -f
+  ln -s $DUN_SOURCES/bin/duniter_js /usr/bin/duniter_js -f
   cd $DUN_SOURCES
   cd node/bin/
   ln -s ../lib/node_modules/npm/bin/npm-cli.js ./npm -f
diff --git a/release/extra/debian/package/DEBIAN/prerm b/release/extra/debian/package/DEBIAN/prerm
index cfa07fcd9..3a6bf2c78 100755
--- a/release/extra/debian/package/DEBIAN/prerm
+++ b/release/extra/debian/package/DEBIAN/prerm
@@ -1,5 +1,6 @@
 #!/bin/bash
 
 [[ -f /usr/bin/duniter ]] && rm /usr/bin/duniter
+[[ -f /usr/bin/duniter_js ]] && rm /usr/bin/duniter_js
 [[ -f /usr/bin/duniter-desktop ]] && rm -f /usr/bin/duniter-desktop
 [[ -d /opt/duniter ]] && rm -Rf /opt/duniter
diff --git a/release/extra/openrc/duniter.initd b/release/extra/openrc/duniter.initd
index ef29b84ee..5a13c8cb5 100644
--- a/release/extra/openrc/duniter.initd
+++ b/release/extra/openrc/duniter.initd
@@ -21,7 +21,7 @@ fi
 if [[ ! -z ${DUNITER_KEYS} ]] && [[ -r ${DUNITER_KEYS} ]]; then
 	command_args="${command_args} --keyfile \"${DUNITER_KEYS}\""
 fi
-command_args="${command_args} --home \"${DUNITER_HOME}\" --mdb \"${DUNITER_DATA}\""
+command_args="--home \"${DUNITER_HOME}\" --mdb \"${DUNITER_DATA}\" ${command_args}"
 start_stop_daemon_args="--user \"${DUNITER_USER}\":\"${DUNITER_GROUP}\" ${DUNITER_SSD_OPTIONS}"
 description="Duniter node"
 
@@ -30,7 +30,7 @@ depend() {
 }
 
 status() {
-	if ${command} status --home "${DUNITER_HOME}" --mdb "${DUNITER_DATA}" | grep -q "is running"; then
+	if ${command} --home "${DUNITER_HOME}" --mdb "${DUNITER_DATA}" status | grep -q "is running"; then
 		einfo "status: started"
 		return 0
 	else
@@ -46,5 +46,5 @@ status() {
 }
 
 stop() {
-	${command} stop --home "${DUNITER_HOME}" --mdb "${DUNITER_DATA}"
+	${command} --home "${DUNITER_HOME}" --mdb "${DUNITER_DATA}" stop
 }
diff --git a/release/extra/systemd/duniter.service b/release/extra/systemd/duniter.service
index 88cbefcf1..f8c1426ef 100644
--- a/release/extra/systemd/duniter.service
+++ b/release/extra/systemd/duniter.service
@@ -13,9 +13,9 @@ Environment="DUNITER_OPTS="
 Group=duniter
 User=duniter
 Type=forking
-ExecStart=/usr/bin/duniter ${DUNITER_WEB}start --home ${DUNITER_HOME} --mdb ${DUNITER_DATA} $DUNITER_OPTS
-ExecReload=/usr/bin/duniter ${DUNITER_WEB}restart --home ${DUNITER_HOME} --mdb ${DUNITER_DATA} $DUNITER_OPTS
-ExecStop=/usr/bin/duniter stop --home ${DUNITER_HOME} --mdb ${DUNITER_DATA}
+ExecStart=/usr/bin/duniter --home ${DUNITER_HOME} --mdb ${DUNITER_DATA} ${DUNITER_WEB}start $DUNITER_OPTS
+ExecReload=/usr/bin/duniter --home ${DUNITER_HOME} --mdb ${DUNITER_DATA} ${DUNITER_WEB}restart $DUNITER_OPTS
+ExecStop=/usr/bin/duniter --home ${DUNITER_HOME} --mdb ${DUNITER_DATA} stop
 Restart=on-failure
 
 [Install]
diff --git a/rust-bins/duniter-launcher/src/config.rs b/rust-bins/duniter-launcher/src/config.rs
new file mode 100644
index 000000000..25acdd272
--- /dev/null
+++ b/rust-bins/duniter-launcher/src/config.rs
@@ -0,0 +1,270 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use std::str::FromStr;
+
+use crate::*;
+
+#[derive(Debug)]
+struct Percent(pub usize);
+
+impl FromStr for Percent {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let usize_: usize = s.parse()?;
+        if usize_ <= 100 {
+            Ok(Self(usize_))
+        } else {
+            Err(anyhow!("A percentage should be <= 100 !"))
+        }
+    }
+}
+
+#[derive(StructOpt)]
+pub(crate) struct DuniterConfigArgs {
+    /// Percent of CPU usage for proof-of-work computation
+    #[structopt(long)]
+    cpu: Option<Percent>,
+    /// Prefix node id for the first character of nonce
+    #[structopt(long)]
+    prefix: Option<usize>,
+    /// Enable BMA API
+    #[structopt(long, display_order(0))]
+    bma: bool,
+    /// Disable BMA API
+    #[structopt(long, display_order(1), alias = "nobma", conflicts_with("bma"))]
+    no_bma: bool,
+    /// Use UPnP to open BMA remote port.
+    #[structopt(long, display_order(2), alias = "upnp")]
+    bma_upnp: bool,
+    /// Do not use UPnP to open BMA remote port.
+    #[structopt(long, display_order(3), alias = "noupnp", conflicts_with("bma-upnp"))]
+    bma_no_upnp: bool,
+    /// Enable WS2P Public access.
+    #[structopt(long, display_order(4))]
+    ws2p_public: bool,
+    /// Disable WS2P Public access.
+    #[structopt(
+        long,
+        display_order(5),
+        alias = "ws2p-nopublic",
+        conflicts_with("ws2p-public")
+    )]
+    ws2p_no_public: bool,
+    /// Use UPnP to open WS2P remote port.
+    #[structopt(long, display_order(6))]
+    ws2p_upnp: bool,
+    /// Do not use UPnP to open WS2P remote port.
+    #[structopt(
+        long,
+        display_order(7),
+        alias = "ws2p-noupnp",
+        conflicts_with("ws2p-upnp")
+    )]
+    ws2p_no_upnp: bool,
+    /// WS2P host to listen to.
+    #[structopt(long)]
+    ws2p_host: Option<String>,
+    /// WS2P port to listen to.
+    #[structopt(long)]
+    ws2p_port: Option<u16>,
+    /// WS2P availabily host.
+    #[structopt(long)]
+    ws2p_remote_host: Option<String>,
+    /// WS2P availabily port.
+    #[structopt(long)]
+    ws2p_remote_port: Option<u16>,
+    /// WS2P availabily path.
+    #[structopt(long)]
+    ws2p_remote_path: Option<String>,
+    /// Maximum outcoming connections count.
+    #[structopt(long)]
+    ws2p_max_private: Option<u8>,
+    /// Maximum incoming connections count.
+    #[structopt(long)]
+    ws2p_max_public: Option<u8>,
+
+    // Hidden options
+    /// Number of cores uses for proof-of-work computation
+    #[structopt(long, hidden(true))]
+    nb_cores: Option<usize>,
+    /// Enable WS2P Private.
+    #[structopt(long, hidden(true))]
+    ws2p_private: bool,
+    /// Disable WS2P Private.
+    #[structopt(
+        long,
+        hidden(true),
+        alias = "ws2p-noprivate",
+        conflicts_with("ws2p-private")
+    )]
+    ws2p_no_private: bool,
+    /// Add a preferred node to connect to through private access.
+    #[structopt(long, hidden(true))]
+    ws2p_prefered_add: Option<String>,
+    /// Remove preferred node.
+    #[structopt(long, hidden(true))]
+    ws2p_prefered_rm: Option<String>,
+    /// Only connect to preferred nodes.
+    #[structopt(long, hidden(true))]
+    ws2p_prefered_only: bool,
+    /// Add a privileged node to for our public access.
+    #[structopt(long, hidden(true))]
+    ws2p_privileged_add: Option<String>,
+    /// Remove privileged node.
+    #[structopt(long, hidden(true))]
+    ws2p_privileged_rm: Option<String>,
+    /// Accept only connections from a privileged node.
+    #[structopt(long, hidden(true))]
+    ws2p_privileged_only: bool,
+    /// Add given endpoint to the list of endpoints of this node.
+    #[structopt(long, hidden(true))]
+    addep: Option<String>,
+    /// Remove given endpoint to the list of endpoints of this node.
+    #[structopt(long, hidden(true))]
+    remep: Option<String>,
+    /// Use Socks Proxy for WS2P Private
+    #[structopt(long, hidden(true), alias = "socks-proxy")]
+    ws2p_socks_proxy: Option<String>,
+    /// Use Tor Socks Proxy
+    #[structopt(long, hidden(true))]
+    tor_proxy: Option<String>,
+    /// Method for reaching an clear endpoint
+    #[structopt(long, hidden(true), possible_values = &["clear", "tor", "none"])]
+    reaching_clear_ep: Option<String>,
+    /// Remove all proxies
+    #[structopt(long, hidden(true))]
+    rm_proxies: bool,
+    /// Force duniter to contact endpoint tor (if you redirect the traffic to tor yourself)
+    #[structopt(long, hidden(true))]
+    force_tor: bool,
+}
+
+pub(crate) fn gen_args(args: &DuniterConfigArgs, duniter_js_args: &mut Vec<String>) {
+    if let Some(Percent(cpu_percent)) = args.cpu {
+        duniter_js_args.push("--cpu".into());
+        duniter_js_args.push(cpu_percent.to_string());
+    }
+    if let Some(nb_cores) = args.nb_cores {
+        duniter_js_args.push("--nb-cores".into());
+        duniter_js_args.push(nb_cores.to_string());
+    }
+    if let Some(prefix) = args.prefix {
+        duniter_js_args.push("--prefix".into());
+        duniter_js_args.push(prefix.to_string());
+    }
+    if args.bma {
+        duniter_js_args.push("--bma".into());
+    } else if args.no_bma {
+        duniter_js_args.push("--nobma".into());
+    }
+    if args.bma_upnp {
+        duniter_js_args.push("--upnp".into());
+    } else if args.bma_no_upnp {
+        duniter_js_args.push("--noupnp".into());
+    }
+    if args.ws2p_upnp {
+        duniter_js_args.push("--ws2p-upnp".into());
+    } else if args.ws2p_no_upnp {
+        duniter_js_args.push("--ws2p-noupnp".into());
+    }
+    if args.ws2p_private {
+        duniter_js_args.push("--ws2p-private".into());
+    } else if args.ws2p_no_private {
+        duniter_js_args.push("--ws2p-noprivate".into());
+    }
+    if args.ws2p_public {
+        duniter_js_args.push("--ws2p-public".into());
+    } else if args.ws2p_no_public {
+        duniter_js_args.push("--ws2p-nopublic".into());
+    }
+    if let Some(ref ws2p_host) = args.ws2p_host {
+        duniter_js_args.push("--ws2p-host".into());
+        duniter_js_args.push(ws2p_host.into());
+    }
+    if let Some(ws2p_port) = args.ws2p_port {
+        duniter_js_args.push("--ws2p-port".into());
+        duniter_js_args.push(ws2p_port.to_string());
+    }
+    if let Some(ref ws2p_remote_host) = args.ws2p_remote_host {
+        duniter_js_args.push("--ws2p-remote-host".into());
+        duniter_js_args.push(ws2p_remote_host.into());
+    }
+    if let Some(ws2p_remote_port) = args.ws2p_remote_port {
+        duniter_js_args.push("--ws2p-remote-port".into());
+        duniter_js_args.push(ws2p_remote_port.to_string());
+    }
+    if let Some(ref ws2p_remote_path) = args.ws2p_remote_path {
+        duniter_js_args.push("--ws2p-remote-path".into());
+        duniter_js_args.push(ws2p_remote_path.into());
+    }
+    if let Some(ref ws2p_max_private) = args.ws2p_max_private {
+        duniter_js_args.push("--ws2p-max-private".into());
+        duniter_js_args.push(ws2p_max_private.to_string());
+    }
+    if let Some(ref ws2p_max_public) = args.ws2p_max_public {
+        duniter_js_args.push("--ws2p-max-public".into());
+        duniter_js_args.push(ws2p_max_public.to_string());
+    }
+    if let Some(ref ws2p_prefered_add) = args.ws2p_prefered_add {
+        duniter_js_args.push("--ws2p-prefered-add".into());
+        duniter_js_args.push(ws2p_prefered_add.to_string());
+    }
+    if let Some(ref ws2p_prefered_rm) = args.ws2p_prefered_rm {
+        duniter_js_args.push("--ws2p-prefered-rm".into());
+        duniter_js_args.push(ws2p_prefered_rm.to_string());
+    }
+    if args.ws2p_prefered_only {
+        duniter_js_args.push("--ws2p-prefered-only".into());
+    }
+    if let Some(ref ws2p_privileged_add) = args.ws2p_privileged_add {
+        duniter_js_args.push("--ws2p-privileged-add".into());
+        duniter_js_args.push(ws2p_privileged_add.to_string());
+    }
+    if let Some(ref ws2p_privileged_rm) = args.ws2p_privileged_rm {
+        duniter_js_args.push("--ws2p-privileged-rm".into());
+        duniter_js_args.push(ws2p_privileged_rm.to_string());
+    }
+    if args.ws2p_privileged_only {
+        duniter_js_args.push("--ws2p-privileged-only".into());
+    }
+    if let Some(ref addep) = args.addep {
+        duniter_js_args.push("--addep".into());
+        duniter_js_args.push(addep.into());
+    }
+    if let Some(ref remep) = args.remep {
+        duniter_js_args.push("--remep".into());
+        duniter_js_args.push(remep.into());
+    }
+    if let Some(ref ws2p_socks_proxy) = args.ws2p_socks_proxy {
+        duniter_js_args.push("--socks-proxy".into());
+        duniter_js_args.push(ws2p_socks_proxy.into());
+    }
+    if let Some(ref tor_proxy) = args.tor_proxy {
+        duniter_js_args.push("--tor-proxy".into());
+        duniter_js_args.push(tor_proxy.into());
+    }
+    if let Some(ref reaching_clear_ep) = args.reaching_clear_ep {
+        duniter_js_args.push("--reaching-clear-ep".into());
+        duniter_js_args.push(reaching_clear_ep.into());
+    }
+    if args.rm_proxies {
+        duniter_js_args.push("--rm-proxies".into());
+    }
+    if args.force_tor {
+        duniter_js_args.push("--force-tor".into());
+    }
+}
diff --git a/rust-bins/duniter-launcher/src/daemon.rs b/rust-bins/duniter-launcher/src/daemon.rs
new file mode 100644
index 000000000..2eae921c4
--- /dev/null
+++ b/rust-bins/duniter-launcher/src/daemon.rs
@@ -0,0 +1,112 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use crate::*;
+
+pub fn start(prod: bool, profile_path: &Path, duniter_js_args: &[String]) -> Result<()> {
+    let stdout = OpenOptions::new()
+        .append(true)
+        .create(true)
+        .open(profile_path.join(LOG_FILE))?;
+    let mut duniter_js_command = Command::new(get_node_path()?);
+    if prod {
+        duniter_js_command.current_dir(DUNITER_JS_CURRENT_DIR);
+    }
+    let mut child = duniter_js_command
+        .args(duniter_js_args)
+        .stdout(stdout)
+        .stderr(Stdio::null())
+        .spawn()?;
+
+    let pid = child.id();
+
+    // Write pid on file
+    {
+        let mut pid_file = File::create(profile_path.join("app.pid"))?;
+        pid_file.write_all(format!("{}\n{}", pid, duniter_js_args.join(" ")).as_bytes())?;
+    }
+
+    println!("Duniter daemon launched (pid: {}).", pid);
+
+    let daemon = Daemon::new().umask(0o000).start();
+
+    if let Err(e) = daemon {
+        eprintln!("Error, {}", e);
+    }
+
+    let status = child.wait().expect("fail to wait child");
+
+    std::process::exit(status.code().unwrap_or_default())
+}
+
+pub fn status(profile_path: &Path) -> Result<()> {
+    let mut pid_file = File::open(profile_path.join("app.pid"))?;
+    let mut pid_file_content = String::new();
+    pid_file.read_to_string(&mut pid_file_content)?;
+    let mut lines = pid_file_content.split('\n');
+    let pid = lines
+        .next()
+        .expect("corrupted pid file")
+        .parse::<i32>()
+        .expect("invalid pid");
+
+    match nix::sys::wait::waitpid(Some(Pid::from_raw(pid)), Some(WaitPidFlag::WNOHANG)) {
+        Ok(WaitStatus::StillAlive) => {
+            println!("Duniter is running using PID {}.", pid);
+            Ok(())
+        }
+        Ok(_) | Err(Error::Sys(Errno::ESRCH)) => {
+            println!("Duniter is not running.");
+            std::process::exit(EXIT_CODE_DUNITER_NOT_RUNNING);
+        }
+        Err(e) => Err(e.into()),
+    }
+}
+
+pub fn stop(profile_path: &Path) -> Result<Vec<String>> {
+    let mut pid_file = File::open(profile_path.join("app.pid"))?;
+    let mut pid_file_content = String::new();
+    pid_file.read_to_string(&mut pid_file_content)?;
+    let mut lines = pid_file_content.split('\n');
+    let pid = lines
+        .next()
+        .expect("corrupted pid file")
+        .parse::<i32>()
+        .expect("invalid pid");
+    let duniter_args: Vec<String> = lines
+        .next()
+        .expect("corrupted pid file")
+        .split(' ')
+        .map(ToOwned::to_owned)
+        .collect();
+
+    match nix::sys::signal::kill(Pid::from_raw(pid), Some(Signal::SIGINT)) {
+        Err(Error::Sys(Errno::ESRCH)) => {
+            println!("Duniter is not running.");
+            Ok(duniter_args)
+        }
+        Err(e) => Err(e.into()),
+        Ok(()) => {
+            println!("Stopping Duniter daemon …");
+            match nix::sys::wait::waitpid(Some(Pid::from_raw(pid)), Some(WaitPidFlag::WSTOPPED)) {
+                Ok(_) | Err(Error::Sys(Errno::ECHILD)) => {
+                    println!("Duniter daemon stopped.");
+                    Ok(duniter_args)
+                }
+                Err(e) => Err(e.into()),
+            }
+        }
+    }
+}
diff --git a/rust-bins/duniter-launcher/src/duniter_ts_args.rs b/rust-bins/duniter-launcher/src/duniter_ts_args.rs
new file mode 100644
index 000000000..5158c83bf
--- /dev/null
+++ b/rust-bins/duniter-launcher/src/duniter_ts_args.rs
@@ -0,0 +1,145 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use crate::*;
+
+fn gen_start_args(args: &DuniterStartArgs, duniter_ts_args: &mut Vec<String>) {
+    if let Some(ref keyfile) = args.keyfile {
+        duniter_ts_args.push("--keyfile".to_owned());
+        duniter_ts_args.push(
+            keyfile
+                .to_str()
+                .expect("keyfile path is invalid")
+                .to_owned(),
+        );
+    }
+}
+
+fn gen_webstart_args(args: &DuniterWebstartArgs, duniter_ts_args: &mut Vec<String>) {
+    if let Some(ref web_ui_host) = args.web_ui_host {
+        duniter_ts_args.push("--webmhost".to_owned());
+        duniter_ts_args.push(web_ui_host.into());
+    }
+    if let Some(ref web_ui_port) = args.web_ui_port {
+        duniter_ts_args.push("--webmport".to_owned());
+        duniter_ts_args.push(web_ui_port.into());
+    }
+}
+
+pub(crate) fn gen_duniter_ts_args(args: &DuniterArgs, duniter_js_exe: String) -> Vec<String> {
+    let mut duniter_ts_args = Vec::new();
+    duniter_ts_args.push(duniter_js_exe);
+    if let Some(ref home) = args.home {
+        duniter_ts_args.push("--mdb".to_owned());
+        duniter_ts_args.push(home.to_str().expect("invalid home path").to_owned());
+    }
+    if let Some(ref log_level) = args.log {
+        duniter_ts_args.push("--loglevel".to_owned());
+        duniter_ts_args.push(log_level.to_string().to_lowercase());
+    }
+    if let Some(ref profile) = args.profile {
+        duniter_ts_args.push("--mdb".to_owned());
+        duniter_ts_args.push(profile.clone());
+    }
+    match args.command {
+        DuniterCommand::Completions { .. } => unreachable!(),
+        DuniterCommand::DirectStart {
+            keyprompt,
+            ref start_args,
+        } => {
+            duniter_ts_args.push("direct_start".to_owned());
+            if keyprompt {
+                duniter_ts_args.push("--keyprompt".to_owned());
+            }
+            gen_start_args(start_args, &mut duniter_ts_args);
+        }
+        DuniterCommand::DirectWebstart {
+            keyprompt,
+            ref start_args,
+            ref webstart_args,
+        } => {
+            duniter_ts_args.push("direct_webstart".to_owned());
+            if keyprompt {
+                duniter_ts_args.push("--keyprompt".to_owned());
+            }
+            gen_start_args(start_args, &mut duniter_ts_args);
+            gen_webstart_args(webstart_args, &mut duniter_ts_args);
+        }
+        DuniterCommand::Start(ref start_args) => {
+            duniter_ts_args.push("direct_start".to_owned());
+            gen_start_args(start_args, &mut duniter_ts_args);
+        }
+        DuniterCommand::Webstart {
+            ref start_args,
+            ref webstart_args,
+        } => {
+            duniter_ts_args.push("direct_webstart".to_owned());
+            gen_start_args(start_args, &mut duniter_ts_args);
+            gen_webstart_args(webstart_args, &mut duniter_ts_args);
+        }
+        DuniterCommand::Stop => duniter_ts_args.push("stop".to_owned()),
+        DuniterCommand::Sync(ref sync_args) => {
+            duniter_ts_args.push("--store-txs".to_owned());
+            duniter_ts_args.push("sync".to_owned());
+            sync::gen_args(sync_args, &mut duniter_ts_args);
+        }
+        DuniterCommand::Config(ref config_args) => {
+            duniter_ts_args.push("config".to_owned());
+            config::gen_args(config_args, &mut duniter_ts_args);
+        }
+        DuniterCommand::Reset(ref reset_command) => {
+            duniter_ts_args.push("reset".to_owned());
+            match reset_command {
+                ResetCommand::Config => duniter_ts_args.push("config".to_owned()),
+                ResetCommand::Data => duniter_ts_args.push("data".to_owned()),
+                ResetCommand::Peers => duniter_ts_args.push("peers".to_owned()),
+                ResetCommand::Stats => duniter_ts_args.push("stats".to_owned()),
+                ResetCommand::All => duniter_ts_args.push("all".to_owned()),
+            }
+        }
+        DuniterCommand::Wizard(ref wizard_command) => {
+            duniter_ts_args.push("wizard".to_owned());
+            match wizard_command {
+                WizardCommand::Bma => duniter_ts_args.push("network".to_owned()),
+                WizardCommand::Key { n, r, p } => {
+                    duniter_ts_args.push("key".to_owned());
+                    if let Some(n) = n {
+                        duniter_ts_args.push("--keyN".to_owned());
+                        duniter_ts_args.push(n.to_string());
+                    }
+                    if let Some(r) = r {
+                        duniter_ts_args.push("--keyr".to_owned());
+                        duniter_ts_args.push(r.to_string());
+                    }
+                    if let Some(p) = p {
+                        duniter_ts_args.push("--keyp".to_owned());
+                        duniter_ts_args.push(p.to_string());
+                    }
+                }
+            }
+        }
+        DuniterCommand::WS2P(ref ws2p_command) => {
+            duniter_ts_args.push("ws2p".to_owned());
+            match ws2p_command {
+                WS2PCommand::ListNodes => duniter_ts_args.push("list-nodes".to_owned()),
+                WS2PCommand::ListPrefered => duniter_ts_args.push("list-prefered".to_owned()),
+                WS2PCommand::ListPrivileged => duniter_ts_args.push("list-privileged".to_owned()),
+                WS2PCommand::ShowConf => duniter_ts_args.push("show-conf".to_owned()),
+            }
+        }
+        DuniterCommand::Logs | DuniterCommand::Restart | DuniterCommand::Status => {}
+    }
+    duniter_ts_args
+}
diff --git a/rust-bins/duniter-launcher/src/main.rs b/rust-bins/duniter-launcher/src/main.rs
new file mode 100644
index 000000000..bdc246701
--- /dev/null
+++ b/rust-bins/duniter-launcher/src/main.rs
@@ -0,0 +1,334 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+#![deny(
+    clippy::unwrap_used,
+    missing_debug_implementations,
+    missing_copy_implementations,
+    trivial_casts,
+    trivial_numeric_casts,
+    unsafe_code,
+    unstable_features,
+    unused_import_braces
+)]
+
+mod config;
+mod daemon;
+mod duniter_ts_args;
+mod sync;
+
+use anyhow::{anyhow, Result};
+use daemonize_me::Daemon;
+use logwatcher::{LogWatcher, LogWatcherAction};
+use nix::{
+    errno::Errno,
+    sys::{signal::Signal, wait::WaitPidFlag, wait::WaitStatus},
+    unistd::Pid,
+    Error,
+};
+use std::{
+    fs::{File, OpenOptions},
+    io::prelude::*,
+    path::Path,
+    path::PathBuf,
+    process::Command,
+    process::Output,
+    process::Stdio,
+};
+use structopt::{clap::Shell, StructOpt};
+
+const APP_NAME: &str = "duniter";
+const DEFAULT_PORT: u16 = 443;
+const DEFAULT_PROFILE: &str = "duniter_default";
+const DUNITER_EXE_PATH: &str = "/opt/duniter/bin/duniter";
+const DUNITER_EXE_LINK_PATH: &str = "/usr/bin/duniter";
+const DUNITER_JS_CURRENT_DIR: &str = "/opt/duniter/";
+const DUNITER_JS_PATH: &str = "/opt/duniter/bin/duniter_js";
+const DUNITER_JS_DEV_PATH: &str = "bin/duniter_js";
+const EMBEDDED_NODE_PATH: &str = "/opt/duniter/node/bin/node";
+const EXIT_CODE_DUNITER_NOT_RUNNING: i32 = 4;
+const LOG_FILE: &str = "duniter.log";
+const NODE_VERSION_BEGIN: &str = "v10.";
+
+#[derive(StructOpt)]
+#[structopt(name = APP_NAME, about = "Crypto-currency software to operate Äž1 libre currency.")]
+struct DuniterArgs {
+    /// Path to Duniter HOME (defaults to "$HOME/.config/duniter").
+    #[structopt(short, long, parse(from_os_str))]
+    home: Option<PathBuf>,
+    /// Logs level (If not specified, use the logs level defined in the configuration or INFO by default).
+    #[structopt(short, long, alias("loglevel"), case_insensitive(true), possible_values = &["ERROR", "WARN", "INFO", "DEBUG", "TRACE"])]
+    log: Option<log::Level>,
+    /// Profile name (defauld "duniter_default")
+    #[structopt(short, long, alias("mdb"))]
+    profile: Option<String>,
+    #[structopt(subcommand)]
+    command: DuniterCommand,
+}
+
+#[derive(StructOpt)]
+#[structopt(rename_all = "snake")]
+enum DuniterCommand {
+    /// Duniter configuration options.
+    #[structopt(
+        display_order(0),
+        after_help("Some advanced options are hidden for readability.")
+    )]
+    Config(Box<config::DuniterConfigArgs>),
+    /// Launch the configuration wizard.
+    #[structopt(display_order(1))]
+    Wizard(WizardCommand),
+    /// WS2P operations for configuration and diagnosis tasks.
+    #[structopt(display_order(2))]
+    WS2P(WS2PCommand),
+    /// Synchronize blockchain from a remote Duniter node.
+    #[structopt(display_order(3))]
+    Sync(sync::DuniterSyncArgs),
+    /// Start Duniter node with direct output, non-daemonized.
+    #[structopt(display_order(4))]
+    DirectStart {
+        /// Force to use the keypair given by user prompt.
+        #[structopt(long)]
+        keyprompt: bool,
+        #[structopt(flatten)]
+        start_args: DuniterStartArgs,
+    },
+    /// Start Duniter node with its web interface with direct output, non-daemonized.
+    #[structopt(display_order(5))]
+    DirectWebstart {
+        /// Force to use the keypair given by user prompt.
+        #[structopt(long)]
+        keyprompt: bool,
+        #[structopt(flatten)]
+        start_args: DuniterStartArgs,
+        #[structopt(flatten)]
+        webstart_args: DuniterWebstartArgs,
+    },
+    /// Starts Duniter as a daemon (background task).
+    #[structopt(display_order(6))]
+    Start(DuniterStartArgs),
+    /// Starts Duniter (with its web interface) as a daemon (background task).
+    #[structopt(display_order(7))]
+    Webstart {
+        #[structopt(flatten)]
+        start_args: DuniterStartArgs,
+        #[structopt(flatten)]
+        webstart_args: DuniterWebstartArgs,
+    },
+    /// Get Duniter daemon status.
+    #[structopt(display_order(8))]
+    Status,
+    /// Follow duniter logs.
+    #[structopt(display_order(9))]
+    Logs,
+    /// Stops Duniter daemon and restart it.
+    #[structopt(display_order(10), alias = "webrestart")]
+    Restart,
+    /// Stops Duniter daemon if it is running.
+    #[structopt(display_order(11))]
+    Stop,
+    /// Reset configuration, data, peers, transactions or everything in the database
+    #[structopt(display_order(12))]
+    Reset(ResetCommand),
+    /// Generate tab-completion script for your shell
+    #[structopt(display_order(13))]
+    Completions {
+        #[structopt(case_insensitive(true), possible_values = &["BASH, FISH, ZSH"])]
+        shell: Shell,
+    },
+}
+
+#[derive(StructOpt)]
+enum ResetCommand {
+    #[structopt(display_order(0))]
+    Config,
+    #[structopt(display_order(1))]
+    Data,
+    #[structopt(display_order(2))]
+    Peers,
+    #[structopt(display_order(3))]
+    Stats,
+    #[structopt(display_order(4))]
+    All,
+}
+
+#[derive(StructOpt)]
+enum WizardCommand {
+    #[structopt(display_order(0))]
+    Key {
+        /// Scrypt `N` CPU/memory cost parameter. Must be a power of 2. Defaults to 4096.
+        #[structopt(short)]
+        n: Option<usize>,
+        /// "Scrypt `r` The blocksize parameter, which fine-tunes sequential memory read size and performance. Defaults to 16."
+        #[structopt(short)]
+        r: Option<usize>,
+        /// Scrypt `p` Parallelization parameter. Defaults to 1.
+        #[structopt(short)]
+        p: Option<usize>,
+    },
+    #[structopt(display_order(1), alias = "network")]
+    Bma,
+}
+
+#[derive(StructOpt)]
+enum WS2PCommand {
+    #[structopt(display_order(0))]
+    ListNodes,
+    #[structopt(display_order(0))]
+    ListPrefered,
+    #[structopt(display_order(0))]
+    ListPrivileged,
+    #[structopt(display_order(0))]
+    ShowConf,
+}
+
+#[derive(StructOpt)]
+struct DuniterStartArgs {
+    /// Force to use the keypair of the given YAML file. File must contain `pub:` and `sec:` fields.
+    #[structopt(long, parse(from_os_str), env("DUNITER_KEYFILE"))]
+    keyfile: Option<PathBuf>,
+}
+
+#[derive(StructOpt)]
+struct DuniterWebstartArgs {
+    /// Web user interface host (IP) to listen to.
+    #[structopt(long, alias = "webmhost", env("DUNITER_WEB_UI_HOST"))]
+    web_ui_host: Option<String>,
+    /// Web user interface port (IP) to listen to.
+    #[structopt(long, alias = "webmport")]
+    web_ui_port: Option<String>,
+}
+
+fn main() -> Result<()> {
+    let args = DuniterArgs::from_args();
+
+    if let DuniterCommand::Completions { shell } = args.command {
+        DuniterArgs::clap().gen_completions_to(APP_NAME, shell, &mut std::io::stdout());
+        Ok(())
+    } else {
+        let profile_path = get_profile_path(args.profile.as_deref())?;
+
+        let current_exe = std::env::current_exe()?;
+        let prod = current_exe == PathBuf::from(DUNITER_EXE_LINK_PATH)
+            || current_exe == PathBuf::from(DUNITER_EXE_PATH);
+
+        let duniter_ts_args = duniter_ts_args::gen_duniter_ts_args(&args, duniter_js_exe()?);
+
+        match args.command {
+            DuniterCommand::Restart => {
+                daemon::start(prod, &profile_path, &daemon::stop(&profile_path)?)
+            }
+            DuniterCommand::Start(_) | DuniterCommand::Webstart { .. } => {
+                daemon::start(prod, &profile_path, &duniter_ts_args)
+            }
+            DuniterCommand::Status => daemon::status(&profile_path),
+            DuniterCommand::Stop => {
+                daemon::stop(&profile_path)?;
+                Ok(())
+            }
+            DuniterCommand::Logs => watch_logs(profile_path),
+            _ => {
+                ctrlc::set_handler(move || {
+                    // This empty handler is necessary otherwise the Rust process is stopped immediately
+                    // without waiting for the child process (duniter_js) to finish stopping.
+                })?;
+                let mut duniter_js_command = Command::new(get_node_path()?);
+                if prod {
+                    duniter_js_command.current_dir(DUNITER_JS_CURRENT_DIR);
+                }
+                let exit_code_opt = duniter_js_command.args(duniter_ts_args).status()?.code();
+                if let Some(exit_code) = exit_code_opt {
+                    std::process::exit(exit_code);
+                } else {
+                    Ok(())
+                }
+            }
+        }
+    }
+}
+
+fn duniter_js_exe() -> Result<String> {
+    let current_exe = std::env::current_exe()?;
+    Ok(
+        if current_exe == PathBuf::from(DUNITER_EXE_LINK_PATH)
+            || current_exe == PathBuf::from(DUNITER_EXE_PATH)
+        {
+            DUNITER_JS_PATH.to_owned()
+        } else {
+            DUNITER_JS_DEV_PATH.to_owned()
+        },
+    )
+}
+
+pub(crate) fn get_node_path() -> Result<&'static str> {
+    let current_exe = std::env::current_exe()?;
+    if current_exe == PathBuf::from(DUNITER_EXE_LINK_PATH)
+        || current_exe == PathBuf::from(DUNITER_EXE_PATH)
+    {
+        let node_path = PathBuf::from(EMBEDDED_NODE_PATH);
+        if node_path.exists() {
+            Ok(EMBEDDED_NODE_PATH)
+        } else {
+            eprintln!("Node.js is not embedded in this version of Duniter");
+            std::process::exit(1);
+        }
+    } else if get_node_version("node")?.starts_with(NODE_VERSION_BEGIN) {
+        Ok("node")
+    } else {
+        eprintln!("Duniter require Node.js v10.x");
+        std::process::exit(1);
+    }
+}
+
+pub(crate) fn get_node_version(node_path: &str) -> Result<String> {
+    let Output {
+        status,
+        stdout,
+        stderr,
+    } = Command::new(node_path).arg("-v").output()?;
+
+    if status.success() {
+        Ok(String::from_utf8(stdout)
+            .unwrap_or_else(|_| "Output is not a valid utf8 string".to_owned()))
+    } else {
+        eprintln!(
+            "{}",
+            String::from_utf8(stderr)
+                .unwrap_or_else(|_| "Error message is not a valid utf8 string".to_owned())
+        );
+        std::process::exit(1);
+    }
+}
+
+fn get_profile_path(profile: Option<&str>) -> Result<PathBuf> {
+    let mut profile_path = dirs::config_dir().expect("unsupported operating system");
+    profile_path.push(APP_NAME);
+    profile_path.push(profile.unwrap_or(DEFAULT_PROFILE));
+    if !profile_path.exists() {
+        std::fs::create_dir_all(&profile_path)?;
+    }
+    Ok(profile_path)
+}
+
+fn watch_logs(profile_path: PathBuf) -> Result<()> {
+    let mut log_watcher = LogWatcher::register(profile_path.join(LOG_FILE))?;
+
+    log_watcher.watch(&mut move |line: String| {
+        println!("{}", line);
+        LogWatcherAction::None
+    });
+
+    Ok(())
+}
diff --git a/rust-bins/duniter-launcher/src/sync.rs b/rust-bins/duniter-launcher/src/sync.rs
new file mode 100644
index 000000000..1e66d2513
--- /dev/null
+++ b/rust-bins/duniter-launcher/src/sync.rs
@@ -0,0 +1,85 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use crate::*;
+
+#[derive(StructOpt)]
+pub(crate) struct DuniterSyncArgs {
+    /// Check all DUPB rules (very long).
+    #[structopt(hidden(true), long)]
+    cautious: bool,
+    /// Allow to synchronize on nodes with local network IP address.
+    #[structopt(hidden(true), long)]
+    localsync: bool,
+    /// Disable interactive sync UI.
+    #[structopt(long, alias = "nointeractive")]
+    no_interactive: bool,
+    /// Do not retrieve peers.
+    #[structopt(long, alias = "nopeers")]
+    no_peers: bool,
+    /// Disables P2P downloading of blocs.
+    #[structopt(long, alias = "nop2p")]
+    no_p2p: bool,
+    /// Do not retrieve sandboxes during sync.
+    #[structopt(long, alias = "nosbx")]
+    no_sandboxes: bool,
+    /// Will only try to sync peers.
+    #[structopt(long, alias = "onlypeers")]
+    only_peers: bool,
+    /// Download slowly the blokchcain (for low connnections).
+    #[structopt(long)]
+    slow: bool,
+    // Host or directory
+    source: String,
+    /// Port
+    port: Option<u16>,
+}
+
+pub(crate) fn gen_args(args: &DuniterSyncArgs, duniter_ts_args: &mut Vec<String>) {
+    if args.source.contains(':') || args.source.contains('/') {
+        duniter_ts_args.push(args.source.clone());
+    } else {
+        duniter_ts_args.push(format!(
+            "{}:{}",
+            args.source,
+            args.port.unwrap_or(DEFAULT_PORT)
+        ));
+    }
+    if args.cautious {
+        duniter_ts_args.push("--cautious".into());
+    }
+    if args.localsync {
+        duniter_ts_args.push("--localsync".into());
+    }
+    if args.no_interactive {
+        duniter_ts_args.push("--nointeractive".into());
+    }
+    if args.no_peers {
+        duniter_ts_args.push("--nopeers".into());
+    }
+    if args.no_p2p {
+        duniter_ts_args.push("--nop2p".into());
+    }
+    if args.no_sandboxes {
+        duniter_ts_args.push("--nosbx".into());
+    }
+    if args.only_peers {
+        duniter_ts_args.push("--onlypeers".into());
+    }
+    if args.slow {
+        duniter_ts_args.push("--slow".into());
+    }
+    todo!()
+}
diff --git a/rust-bins/xtask/Cargo.toml b/rust-bins/xtask/Cargo.toml
new file mode 100644
index 000000000..95175adcd
--- /dev/null
+++ b/rust-bins/xtask/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+authors = ["elois <elois@duniter.org>"]
+description = "Duniter xtask."
+edition = "2018"
+license = "AGPL-3.0"
+name = "xtask"
+repository = "https://git.duniter.org/nodes/typescript/duniter"
+version = "0.1.0"
+
+[[bin]]
+bench = false
+path = "src/main.rs"
+name = "xtask"
+
+[dependencies]
+structopt = "0.3.18"
+version_check = "0.9.2"
diff --git a/rust-bins/xtask/src/main.rs b/rust-bins/xtask/src/main.rs
new file mode 100644
index 000000000..91f45095a
--- /dev/null
+++ b/rust-bins/xtask/src/main.rs
@@ -0,0 +1,145 @@
+//  Copyright (C) 2020 Éloïs SANCHEZ.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use std::{
+    io::Result,
+    process::{Command, Output},
+};
+use structopt::StructOpt;
+
+const MIN_RUST_VERTION: &str = "1.46.0";
+const NODE_VERSION: &str = "10.22.1";
+const NVM_VERSION: &str = "0.35.3";
+
+#[derive(StructOpt)]
+struct DuniterXTask {
+    #[structopt(long)]
+    skip_npm: bool,
+    #[structopt(subcommand)]
+    command: DuniterXTaskCommand,
+}
+
+#[derive(StructOpt)]
+enum DuniterXTaskCommand {
+    Build {
+        #[structopt(long)]
+        production: bool,
+    },
+    Test,
+}
+
+fn main() -> Result<()> {
+    let args = DuniterXTask::from_args();
+
+    if !version_check::is_min_version(MIN_RUST_VERTION).unwrap_or(false)
+        && exec_should_success(Command::new("rustup").args(&["update", "stable"])).is_err()
+    {
+        eprintln!(
+                "Duniter requires Rust {} or higher. If you installed the Rust toolchain via rustup, please execute the command `rustup update stable`.",
+                MIN_RUST_VERTION
+            );
+        std::process::exit(1);
+    }
+    Command::new("rustc").arg("--version").status()?;
+    Command::new("cargo").arg("--version").status()?;
+
+    if !args.skip_npm {
+        println!("Check node version …");
+        if exec_and_get_stdout(Command::new("node").arg("-v"))
+            .unwrap_or_default()
+            .trim_end()
+            != format!("v{}", NODE_VERSION)
+        {
+            println!("Install node v{} …", NODE_VERSION);
+            install_and_use_node_version()?;
+        } else {
+            println!("Node v{} already installed ✔", NODE_VERSION);
+        }
+    }
+    match args.command {
+        DuniterXTaskCommand::Build { production } => build(args.skip_npm, production),
+        DuniterXTaskCommand::Test => test(args.skip_npm),
+    }
+}
+
+fn install_and_use_node_version() -> Result<()> {
+    if exec_should_success(Command::new("nvm").arg("--version")).is_err() {
+        println!("Install nvm v{} …", NVM_VERSION);
+        let nvm_install_script = exec_and_get_stdout(Command::new("wget").args(&[
+            "-qO-",
+            &format!(
+                "https://raw.githubusercontent.com/nvm-sh/nvm/v{}/install.sh",
+                NVM_VERSION
+            ),
+        ]))?;
+        exec_should_success(Command::new("bash").arg(nvm_install_script))?;
+    }
+    exec_should_success(Command::new("nvm").args(&["install", NODE_VERSION]))?;
+    exec_should_success(Command::new("nvm").args(&["use", NODE_VERSION]))
+}
+
+fn exec_and_get_stdout(command: &mut Command) -> Result<String> {
+    let Output {
+        status,
+        stdout,
+        stderr,
+    } = command.output()?;
+
+    if status.success() {
+        Ok(String::from_utf8(stdout)
+            .unwrap_or_else(|_| "Output is not a valid utf8 string".to_owned()))
+    } else {
+        eprintln!(
+            "{}",
+            String::from_utf8(stderr)
+                .unwrap_or_else(|_| "Error message is not a valid utf8 string".to_owned())
+        );
+        std::process::exit(1);
+    }
+}
+
+fn exec_should_success(command: &mut Command) -> Result<()> {
+    if !command.status()?.success() {
+        std::process::exit(1);
+    } else {
+        Ok(())
+    }
+}
+
+fn build(skip_npm: bool, production: bool) -> Result<()> {
+    if !skip_npm {
+        exec_should_success(Command::new("npm").args(&["add", "duniter-ui"]))?;
+        exec_should_success(
+            Command::new("npm")
+                .env("NEON_BUILD_RELEASE", "true")
+                .arg("install"),
+        )?;
+        if production {
+            exec_should_success(Command::new("npm").args(&["prune", "--production"]))?;
+        }
+    }
+    println!("Build duniter-launcher …");
+    exec_should_success(Command::new("cargo").args(&["build", "--release"]))?;
+    std::fs::copy("target/release/duniter", "bin/duniter")?;
+    Ok(())
+}
+
+fn test(skip_npm: bool) -> Result<()> {
+    exec_should_success(Command::new("cargo").args(&["test", "--all"]))?;
+    if !skip_npm {
+        exec_should_success(Command::new("npm").arg("test"))?;
+    }
+    Ok(())
+}
diff --git a/server.ts b/server.ts
index ee920422d..6f8ed1ea1 100644
--- a/server.ts
+++ b/server.ts
@@ -585,7 +585,7 @@ export class Server extends stream.Duplex implements HookableServer {
    * the script arguments.
    *
    *   Ex:
-   *     * process.argv: ['/usr/bin/node', '/opt/duniter/sources/bin/duniter', 'restart', '--mdb', 'g1']
+   *     * process.argv: ['/usr/bin/node', '/opt/duniter/sources/bin/duniter_js', 'restart', '--mdb', 'g1']
    *
    *     Then `getCommand('direct_start', 'restart') will return:
    *
@@ -593,7 +593,7 @@ export class Server extends stream.Duplex implements HookableServer {
    *
    *     This new array is what will be given to a *fork* of current script, resulting in a new process with:
    *
-   *     * process.argv: ['/usr/bin/node', '/opt/duniter/sources/bin/duniter', 'direct_start', '--mdb', 'g1']
+   *     * process.argv: ['/usr/bin/node', '/opt/duniter/sources/bin/duniter_js', 'direct_start', '--mdb', 'g1']
    *
    * @param cmd
    * @param insteadOfCmd
diff --git a/test/integration/misc/cli.ts b/test/integration/misc/cli.ts
index 070d6671c..9f142e4b3 100644
--- a/test/integration/misc/cli.ts
+++ b/test/integration/misc/cli.ts
@@ -131,23 +131,6 @@ describe("CLI", function() {
     res[res.length - 1].should.have.property('number').equal(7);
     res.should.have.length(7 + 1); // blocks #0..#7
   })
-
-  // it('sync 4 blocks (cautious)', async () => {
-  //   await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), '--nointeractive', '11']);
-  //   const res = await execute(['export-bc', '--nostdout']);
-  //   res[res.length - 1].should.have.property('number').equal(11);
-  //   res.should.have.length(11 + 1);
-  // })
-  //
-  // it('[spawn] reset data', async () => {
-  //   await executeSpawn(['reset', 'data']);
-  //   const res = await executeSpawn(['export-bc']);
-  //   JSON.parse(res).should.have.length(0);
-  // })
-  //
-  // it('[spawn] sync 10 first blocks --memory', async () => {
-  //   await execute(['sync', fakeServer.host + ':' + String(fakeServer.port), '--memory', '--cautious', '--nointeractive', '10']);
-  // })
 });
 
 /**
@@ -161,23 +144,3 @@ async function execute(args:(string)[]) {
   // Executes the command
   return stack.executeStack(finalArgs);
 }
-
-/**
- * Executes a duniter command, as a command line utility.
- * @param command Array of arguments.
- * @returns {*|Promise} Returns the command output.
- */
-async function executeSpawn(command:string[]): Promise<string> {
-  const finalArgs = [path.join(__dirname, '../../../bin/duniter')].concat(command).concat(['--mdb', DB_NAME]);
-  const duniterCmd = spawn(process.argv[0], finalArgs);
-  return new Promise<string>((resolve, reject) => {
-    let res = "";
-    duniterCmd.stdout.on('data', (data:any) => {
-      res += data.toString('utf8').replace(/\n/, '');
-    });
-    duniterCmd.stderr.on('data', (err:any) => {
-      console.log(err.toString('utf8').replace(/\n/, ''));
-    });
-    duniterCmd.on('close', (code:any) => code ? reject(code) : resolve(res) );
-  });
-}
diff --git a/test/run.sh b/test/run.sh
deleted file mode 100755
index 4f22f7774..000000000
--- a/test/run.sh
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-
-if [ "${1}" = "rs" ]
-then
-	$HOME/.cargo/bin/cargo test --all
-elif [ "${1}" = "ts" ]
-then
-    nyc --reporter html mocha
-else
-    $HOME/.cargo/bin/cargo test --all && nyc --reporter html mocha
-fi
-- 
GitLab