elilchen лет назад: 3
Родитель
Сommit
7a0e300ff9
44 измененных файлов с 15162 добавлено и 2 удалено
  1. 20 2
      .github/workflows/build.yaml
  2. 1 0
      Cargo.toml
  3. 4 0
      ui/.gitignore
  4. 3787 0
      ui/Cargo.lock
  5. 31 0
      ui/Cargo.toml
  6. 21 0
      ui/build.rs
  7. 344 0
      ui/html/editor/codemirror.css
  8. 9874 0
      ui/html/editor/codemirror.js
  9. 88 0
      ui/html/editor/toml.js
  10. 22 0
      ui/html/index.html
  11. 153 0
      ui/html/main.js
  12. 34 0
      ui/html/style.css
  13. BIN
      ui/icons/128x128.png
  14. BIN
      ui/icons/128x128@2x.png
  15. BIN
      ui/icons/32x32.png
  16. BIN
      ui/icons/Square107x107Logo.png
  17. BIN
      ui/icons/Square142x142Logo.png
  18. BIN
      ui/icons/Square150x150Logo.png
  19. BIN
      ui/icons/Square284x284Logo.png
  20. BIN
      ui/icons/Square30x30Logo.png
  21. BIN
      ui/icons/Square310x310Logo.png
  22. BIN
      ui/icons/Square44x44Logo.png
  23. BIN
      ui/icons/Square71x71Logo.png
  24. BIN
      ui/icons/Square89x89Logo.png
  25. BIN
      ui/icons/StoreLogo.png
  26. BIN
      ui/icons/icon.icns
  27. BIN
      ui/icons/icon.ico
  28. BIN
      ui/icons/icon.png
  29. 108 0
      ui/setup.nsi
  30. BIN
      ui/setup/service/nssm.exe
  31. 23 0
      ui/setup/service/run.cmd
  32. 5 0
      ui/src/adapter/mod.rs
  33. 3 0
      ui/src/adapter/service/mod.rs
  34. 130 0
      ui/src/adapter/service/windows.rs
  35. 220 0
      ui/src/adapter/view/desktop.rs
  36. 3 0
      ui/src/adapter/view/mod.rs
  37. 17 0
      ui/src/lib.rs
  38. 25 0
      ui/src/main.rs
  39. 9 0
      ui/src/usecase/mod.rs
  40. 59 0
      ui/src/usecase/presenter.rs
  41. 24 0
      ui/src/usecase/service.rs
  42. 22 0
      ui/src/usecase/view.rs
  43. 46 0
      ui/src/usecase/watcher.rs
  44. 89 0
      ui/tauri.conf.json

+ 20 - 2
.github/workflows/build.yaml

@@ -95,7 +95,24 @@ jobs:
95 95
         with:
96 96
           command: build
97 97
           args: --release --all-features --target=x86_64-pc-windows-msvc
98
-          use-cross: true  
98
+          use-cross: true
99
+
100
+      - name: Install NSIS
101
+        run: |
102
+          iwr -useb get.scoop.sh -outfile 'install.ps1'
103
+          .\install.ps1 -RunAsAdmin
104
+          scoop update
105
+          scoop bucket add extras
106
+          scoop install nsis
107
+
108
+      - run: rustup default nightly
109
+      - run: cargo build --release
110
+        working-directory: ./ui
111
+      - run: xcopy /y target\x86_64-pc-windows-msvc\release\*.exe ui\setup\bin\
112
+      - run: xcopy /y ui\target\release\*.exe ui\setup\
113
+      - run: mkdir ui\setup\logs
114
+      - run: makensis /V1 setup.nsi
115
+        working-directory: ./ui
99 116
 
100 117
       - name: Publish Artifacts
101 118
         uses: actions/upload-artifact@v3
@@ -104,7 +121,8 @@ jobs:
104 121
           path: |
105 122
             target\x86_64-pc-windows-msvc\release\hbbr.exe
106 123
             target\x86_64-pc-windows-msvc\release\hbbs.exe
107
-            target\x86_64-pc-windows-msvc\release\rustdesk-utils.exe 
124
+            target\x86_64-pc-windows-msvc\release\rustdesk-utils.exe
125
+            ui\RustDeskServer.Setup.exe
108 126
           if-no-files-found: error
109 127
 
110 128
   # github (draft) release with all binaries

+ 1 - 0
Cargo.toml

@@ -57,3 +57,4 @@ hbb_common = { path = "libs/hbb_common" }
57 57
 
58 58
 [workspace]
59 59
 members = ["libs/hbb_common"]
60
+exclude = ["ui"]

+ 4 - 0
ui/.gitignore

@@ -0,0 +1,4 @@
1
+# Generated by Cargo
2
+# will have compiled files and executables
3
+/target/
4
+

Разница между файлами не показана из-за своего большого размера
+ 3787 - 0
ui/Cargo.lock


+ 31 - 0
ui/Cargo.toml

@@ -0,0 +1,31 @@
1
+[package]
2
+name = "rustdesk_server"
3
+version = "0.1.1"
4
+description = "rustdesk server gui"
5
+authors = ["elilchen"]
6
+edition = "2021"
7
+
8
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
9
+
10
+[build-dependencies]
11
+tauri-build = { version = "1.2", features = [] }
12
+winres = "0.1"
13
+
14
+[dependencies]
15
+async-std = { version = "1.12", features = ["attributes", "unstable"] }
16
+crossbeam-channel = "0.5"
17
+derive-new = "0.5"
18
+notify = "5.1"
19
+once_cell = "1.17"
20
+serde_json = "1.0"
21
+serde = { version = "1.0", features = ["derive"] }
22
+tauri = { version = "1.2", features = ["fs-exists", "fs-read-dir", "fs-read-file", "fs-write-file", "path-all", "shell-open", "system-tray"] }
23
+windows-service = "0.5.0"
24
+
25
+[features]
26
+# by default Tauri runs in production mode
27
+# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
28
+default = ["custom-protocol"]
29
+# this feature is used used for production builds where `devPath` points to the filesystem
30
+# DO NOT remove this
31
+custom-protocol = ["tauri/custom-protocol"]

+ 21 - 0
ui/build.rs

@@ -0,0 +1,21 @@
1
+fn main() {
2
+  tauri_build::build();
3
+    if cfg!(target_os = "windows") {
4
+        let mut res = winres::WindowsResource::new();
5
+        res.set_icon("icons\\icon.ico");
6
+        res.set_manifest(
7
+            r#"
8
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
9
+<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
10
+    <security>
11
+        <requestedPrivileges>
12
+            <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
13
+        </requestedPrivileges>
14
+    </security>
15
+</trustInfo>
16
+</assembly>
17
+"#,
18
+        );
19
+        res.compile().unwrap();
20
+    }
21
+}

+ 344 - 0
ui/html/editor/codemirror.css

@@ -0,0 +1,344 @@
1
+/* BASICS */
2
+
3
+.CodeMirror {
4
+  /* Set height, width, borders, and global font properties here */
5
+  font-family: monospace;
6
+  height: 300px;
7
+  color: black;
8
+  direction: ltr;
9
+}
10
+
11
+/* PADDING */
12
+
13
+.CodeMirror-lines {
14
+  padding: 4px 0; /* Vertical padding around content */
15
+}
16
+.CodeMirror pre.CodeMirror-line,
17
+.CodeMirror pre.CodeMirror-line-like {
18
+  padding: 0 4px; /* Horizontal padding of content */
19
+}
20
+
21
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
22
+  background-color: white; /* The little square between H and V scrollbars */
23
+}
24
+
25
+/* GUTTER */
26
+
27
+.CodeMirror-gutters {
28
+  border-right: 1px solid #ddd;
29
+  background-color: #f7f7f7;
30
+  white-space: nowrap;
31
+}
32
+.CodeMirror-linenumbers {}
33
+.CodeMirror-linenumber {
34
+  padding: 0 3px 0 5px;
35
+  min-width: 20px;
36
+  text-align: right;
37
+  color: #999;
38
+  white-space: nowrap;
39
+}
40
+
41
+.CodeMirror-guttermarker { color: black; }
42
+.CodeMirror-guttermarker-subtle { color: #999; }
43
+
44
+/* CURSOR */
45
+
46
+.CodeMirror-cursor {
47
+  border-left: 1px solid black;
48
+  border-right: none;
49
+  width: 0;
50
+}
51
+/* Shown when moving in bi-directional text */
52
+.CodeMirror div.CodeMirror-secondarycursor {
53
+  border-left: 1px solid silver;
54
+}
55
+.cm-fat-cursor .CodeMirror-cursor {
56
+  width: auto;
57
+  border: 0 !important;
58
+  background: #7e7;
59
+}
60
+.cm-fat-cursor div.CodeMirror-cursors {
61
+  z-index: 1;
62
+}
63
+.cm-fat-cursor .CodeMirror-line::selection,
64
+.cm-fat-cursor .CodeMirror-line > span::selection, 
65
+.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
66
+.cm-fat-cursor .CodeMirror-line::-moz-selection,
67
+.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
68
+.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
69
+.cm-fat-cursor { caret-color: transparent; }
70
+@-moz-keyframes blink {
71
+  0% {}
72
+  50% { background-color: transparent; }
73
+  100% {}
74
+}
75
+@-webkit-keyframes blink {
76
+  0% {}
77
+  50% { background-color: transparent; }
78
+  100% {}
79
+}
80
+@keyframes blink {
81
+  0% {}
82
+  50% { background-color: transparent; }
83
+  100% {}
84
+}
85
+
86
+/* Can style cursor different in overwrite (non-insert) mode */
87
+.CodeMirror-overwrite .CodeMirror-cursor {}
88
+
89
+.cm-tab { display: inline-block; text-decoration: inherit; }
90
+
91
+.CodeMirror-rulers {
92
+  position: absolute;
93
+  left: 0; right: 0; top: -50px; bottom: 0;
94
+  overflow: hidden;
95
+}
96
+.CodeMirror-ruler {
97
+  border-left: 1px solid #ccc;
98
+  top: 0; bottom: 0;
99
+  position: absolute;
100
+}
101
+
102
+/* DEFAULT THEME */
103
+
104
+.cm-s-default .cm-header {color: blue;}
105
+.cm-s-default .cm-quote {color: #090;}
106
+.cm-negative {color: #d44;}
107
+.cm-positive {color: #292;}
108
+.cm-header, .cm-strong {font-weight: bold;}
109
+.cm-em {font-style: italic;}
110
+.cm-link {text-decoration: underline;}
111
+.cm-strikethrough {text-decoration: line-through;}
112
+
113
+.cm-s-default .cm-keyword {color: #708;}
114
+.cm-s-default .cm-atom {color: #219;}
115
+.cm-s-default .cm-number {color: #164;}
116
+.cm-s-default .cm-def {color: #00f;}
117
+.cm-s-default .cm-variable,
118
+.cm-s-default .cm-punctuation,
119
+.cm-s-default .cm-property,
120
+.cm-s-default .cm-operator {}
121
+.cm-s-default .cm-variable-2 {color: #05a;}
122
+.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
123
+.cm-s-default .cm-comment {color: #a50;}
124
+.cm-s-default .cm-string {color: #a11;}
125
+.cm-s-default .cm-string-2 {color: #f50;}
126
+.cm-s-default .cm-meta {color: #555;}
127
+.cm-s-default .cm-qualifier {color: #555;}
128
+.cm-s-default .cm-builtin {color: #30a;}
129
+.cm-s-default .cm-bracket {color: #997;}
130
+.cm-s-default .cm-tag {color: #170;}
131
+.cm-s-default .cm-attribute {color: #00c;}
132
+.cm-s-default .cm-hr {color: #999;}
133
+.cm-s-default .cm-link {color: #00c;}
134
+
135
+.cm-s-default .cm-error {color: #f00;}
136
+.cm-invalidchar {color: #f00;}
137
+
138
+.CodeMirror-composing { border-bottom: 2px solid; }
139
+
140
+/* Default styles for common addons */
141
+
142
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
143
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
144
+.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
145
+.CodeMirror-activeline-background {background: #e8f2ff;}
146
+
147
+/* STOP */
148
+
149
+/* The rest of this file contains styles related to the mechanics of
150
+   the editor. You probably shouldn't touch them. */
151
+
152
+.CodeMirror {
153
+  position: relative;
154
+  overflow: hidden;
155
+  background: white;
156
+}
157
+
158
+.CodeMirror-scroll {
159
+  overflow: scroll !important; /* Things will break if this is overridden */
160
+  /* 50px is the magic margin used to hide the element's real scrollbars */
161
+  /* See overflow: hidden in .CodeMirror */
162
+  margin-bottom: -50px; margin-right: -50px;
163
+  padding-bottom: 50px;
164
+  height: 100%;
165
+  outline: none; /* Prevent dragging from highlighting the element */
166
+  position: relative;
167
+  z-index: 0;
168
+}
169
+.CodeMirror-sizer {
170
+  position: relative;
171
+  border-right: 50px solid transparent;
172
+}
173
+
174
+/* The fake, visible scrollbars. Used to force redraw during scrolling
175
+   before actual scrolling happens, thus preventing shaking and
176
+   flickering artifacts. */
177
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
178
+  position: absolute;
179
+  z-index: 6;
180
+  display: none;
181
+  outline: none;
182
+}
183
+.CodeMirror-vscrollbar {
184
+  right: 0; top: 0;
185
+  overflow-x: hidden;
186
+  overflow-y: scroll;
187
+}
188
+.CodeMirror-hscrollbar {
189
+  bottom: 0; left: 0;
190
+  overflow-y: hidden;
191
+  overflow-x: scroll;
192
+}
193
+.CodeMirror-scrollbar-filler {
194
+  right: 0; bottom: 0;
195
+}
196
+.CodeMirror-gutter-filler {
197
+  left: 0; bottom: 0;
198
+}
199
+
200
+.CodeMirror-gutters {
201
+  position: absolute; left: 0; top: 0;
202
+  min-height: 100%;
203
+  z-index: 3;
204
+}
205
+.CodeMirror-gutter {
206
+  white-space: normal;
207
+  height: 100%;
208
+  display: inline-block;
209
+  vertical-align: top;
210
+  margin-bottom: -50px;
211
+}
212
+.CodeMirror-gutter-wrapper {
213
+  position: absolute;
214
+  z-index: 4;
215
+  background: none !important;
216
+  border: none !important;
217
+}
218
+.CodeMirror-gutter-background {
219
+  position: absolute;
220
+  top: 0; bottom: 0;
221
+  z-index: 4;
222
+}
223
+.CodeMirror-gutter-elt {
224
+  position: absolute;
225
+  cursor: default;
226
+  z-index: 4;
227
+}
228
+.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
229
+.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
230
+
231
+.CodeMirror-lines {
232
+  cursor: text;
233
+  min-height: 1px; /* prevents collapsing before first draw */
234
+}
235
+.CodeMirror pre.CodeMirror-line,
236
+.CodeMirror pre.CodeMirror-line-like {
237
+  /* Reset some styles that the rest of the page might have set */
238
+  -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
239
+  border-width: 0;
240
+  background: transparent;
241
+  font-family: inherit;
242
+  font-size: inherit;
243
+  margin: 0;
244
+  white-space: pre;
245
+  word-wrap: normal;
246
+  line-height: inherit;
247
+  color: inherit;
248
+  z-index: 2;
249
+  position: relative;
250
+  overflow: visible;
251
+  -webkit-tap-highlight-color: transparent;
252
+  -webkit-font-variant-ligatures: contextual;
253
+  font-variant-ligatures: contextual;
254
+}
255
+.CodeMirror-wrap pre.CodeMirror-line,
256
+.CodeMirror-wrap pre.CodeMirror-line-like {
257
+  word-wrap: break-word;
258
+  white-space: pre-wrap;
259
+  word-break: normal;
260
+}
261
+
262
+.CodeMirror-linebackground {
263
+  position: absolute;
264
+  left: 0; right: 0; top: 0; bottom: 0;
265
+  z-index: 0;
266
+}
267
+
268
+.CodeMirror-linewidget {
269
+  position: relative;
270
+  z-index: 2;
271
+  padding: 0.1px; /* Force widget margins to stay inside of the container */
272
+}
273
+
274
+.CodeMirror-widget {}
275
+
276
+.CodeMirror-rtl pre { direction: rtl; }
277
+
278
+.CodeMirror-code {
279
+  outline: none;
280
+}
281
+
282
+/* Force content-box sizing for the elements where we expect it */
283
+.CodeMirror-scroll,
284
+.CodeMirror-sizer,
285
+.CodeMirror-gutter,
286
+.CodeMirror-gutters,
287
+.CodeMirror-linenumber {
288
+  -moz-box-sizing: content-box;
289
+  box-sizing: content-box;
290
+}
291
+
292
+.CodeMirror-measure {
293
+  position: absolute;
294
+  width: 100%;
295
+  height: 0;
296
+  overflow: hidden;
297
+  visibility: hidden;
298
+}
299
+
300
+.CodeMirror-cursor {
301
+  position: absolute;
302
+  pointer-events: none;
303
+}
304
+.CodeMirror-measure pre { position: static; }
305
+
306
+div.CodeMirror-cursors {
307
+  visibility: hidden;
308
+  position: relative;
309
+  z-index: 3;
310
+}
311
+div.CodeMirror-dragcursors {
312
+  visibility: visible;
313
+}
314
+
315
+.CodeMirror-focused div.CodeMirror-cursors {
316
+  visibility: visible;
317
+}
318
+
319
+.CodeMirror-selected { background: #d9d9d9; }
320
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
321
+.CodeMirror-crosshair { cursor: crosshair; }
322
+.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
323
+.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
324
+
325
+.cm-searching {
326
+  background-color: #ffa;
327
+  background-color: rgba(255, 255, 0, .4);
328
+}
329
+
330
+/* Used to force a border model for a node */
331
+.cm-force-border { padding-right: .1px; }
332
+
333
+@media print {
334
+  /* Hide the cursor when printing */
335
+  .CodeMirror div.CodeMirror-cursors {
336
+    visibility: hidden;
337
+  }
338
+}
339
+
340
+/* See issue #2901 */
341
+.cm-tab-wrap-hack:after { content: ''; }
342
+
343
+/* Help users use markselection to safely style text background */
344
+span.CodeMirror-selectedtext { background: none; }

Разница между файлами не показана из-за своего большого размера
+ 9874 - 0
ui/html/editor/codemirror.js


+ 88 - 0
ui/html/editor/toml.js

@@ -0,0 +1,88 @@
1
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
2
+// Distributed under an MIT license: https://codemirror.net/5/LICENSE
3
+
4
+(function(mod) {
5
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
6
+    mod(require("../../lib/codemirror"));
7
+  else if (typeof define == "function" && define.amd) // AMD
8
+    define(["../../lib/codemirror"], mod);
9
+  else // Plain browser env
10
+    mod(CodeMirror);
11
+})(function(CodeMirror) {
12
+"use strict";
13
+
14
+CodeMirror.defineMode("toml", function () {
15
+  return {
16
+    startState: function () {
17
+      return {
18
+        inString: false,
19
+        stringType: "",
20
+        lhs: true,
21
+        inArray: 0
22
+      };
23
+    },
24
+    token: function (stream, state) {
25
+      //check for state changes
26
+      if (!state.inString && ((stream.peek() == '"') || (stream.peek() == "'"))) {
27
+        state.stringType = stream.peek();
28
+        stream.next(); // Skip quote
29
+        state.inString = true; // Update state
30
+      }
31
+      if (stream.sol() && state.inArray === 0) {
32
+        state.lhs = true;
33
+      }
34
+      //return state
35
+      if (state.inString) {
36
+        while (state.inString && !stream.eol()) {
37
+          if (stream.peek() === state.stringType) {
38
+            stream.next(); // Skip quote
39
+            state.inString = false; // Clear flag
40
+          } else if (stream.peek() === '\\') {
41
+            stream.next();
42
+            stream.next();
43
+          } else {
44
+            stream.match(/^.[^\\\"\']*/);
45
+          }
46
+        }
47
+        return state.lhs ? "property string" : "string"; // Token style
48
+      } else if (state.inArray && stream.peek() === ']') {
49
+        stream.next();
50
+        state.inArray--;
51
+        return 'bracket';
52
+      } else if (state.lhs && stream.peek() === '[' && stream.skipTo(']')) {
53
+        stream.next();//skip closing ]
54
+        // array of objects has an extra open & close []
55
+        if (stream.peek() === ']') stream.next();
56
+        return "atom";
57
+      } else if (stream.peek() === "#") {
58
+        stream.skipToEnd();
59
+        return "comment";
60
+      } else if (stream.eatSpace()) {
61
+        return null;
62
+      } else if (state.lhs && stream.eatWhile(function (c) { return c != '=' && c != ' '; })) {
63
+        return "property";
64
+      } else if (state.lhs && stream.peek() === "=") {
65
+        stream.next();
66
+        state.lhs = false;
67
+        return null;
68
+      } else if (!state.lhs && stream.match(/^\d\d\d\d[\d\-\:\.T]*Z/)) {
69
+        return 'atom'; //date
70
+      } else if (!state.lhs && (stream.match('true') || stream.match('false'))) {
71
+        return 'atom';
72
+      } else if (!state.lhs && stream.peek() === '[') {
73
+        state.inArray++;
74
+        stream.next();
75
+        return 'bracket';
76
+      } else if (!state.lhs && stream.match(/^\-?\d+(?:\.\d+)?/)) {
77
+        return 'number';
78
+      } else if (!stream.eatSpace()) {
79
+        stream.next();
80
+      }
81
+      return null;
82
+    }
83
+  };
84
+});
85
+
86
+CodeMirror.defineMIME('text/x-toml', 'toml');
87
+
88
+});

+ 22 - 0
ui/html/index.html

@@ -0,0 +1,22 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+<meta charset="utf-8" />
5
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+<title>RustDesk Server</title>
7
+<link rel="icon" href="data:;base64,=">
8
+<script>addEventListener('contextmenu', e => e.preventDefault());</script>
9
+<link rel="stylesheet" href="editor/codemirror.css">
10
+<link rel="stylesheet" href="style.css" />
11
+<script src="editor/codemirror.js"></script>
12
+<script src="editor/toml.js"></script>
13
+<script type="module" src="/main.js" defer></script>
14
+</head>
15
+<body>
16
+<textarea></textarea>
17
+<form>
18
+<label><input type="checkbox"> <p>Turn on auto scroll</p></label>
19
+<label><p>Press ctrl + s to save</p></label>
20
+</form>
21
+</body>
22
+</html>

+ 153 - 0
ui/html/main.js

@@ -0,0 +1,153 @@
1
+const { event, fs, path, tauri } = window.__TAURI__;
2
+
3
+class View {
4
+    constructor() {
5
+        Object.assign(this, {
6
+            content: '',
7
+            action_time: 0,
8
+            is_auto_scroll: true,
9
+            is_edit_mode: false,
10
+            is_file_changed: false,
11
+            is_form_changed: false,
12
+            is_content_changed: false
13
+        }, ...arguments);
14
+        addEventListener('DOMContentLoaded', this.init.bind(this));
15
+    }
16
+    async init() {
17
+        this.editor = this.renderEditor();
18
+        this.editor.on('scroll', this.editorScroll.bind(this));
19
+        this.editor.on('keypress', this.editorSave.bind(this));
20
+        this.form = this.renderForm();
21
+        this.form.addEventListener('change', this.formChange.bind(this));
22
+        event.listen('__update__', this.appAction.bind(this));
23
+        event.emit('__action__', '__init__');
24
+        while (true) {
25
+            try {
26
+                await this.update();
27
+                this.render();
28
+            } catch (e) {
29
+                console.error(e);
30
+            }
31
+            await new Promise(r => setTimeout(r, 100));
32
+        }
33
+    }
34
+    async update() {
35
+        if (this.is_file_changed) {
36
+            this.is_file_changed = false;
37
+            let now = Date.now(),
38
+                file = await path.resolveResource(this.file);
39
+            if (await fs.exists(file)) {
40
+                let content = await fs.readTextFile(file);
41
+                if (this.action_time < now) {
42
+                    this.content = content;
43
+                    this.is_content_changed = true;
44
+                }
45
+            } else {
46
+                if (now >= this.action_time) {
47
+                    if (this.is_edit_mode) {
48
+                        this.content = `# https://github.com/rustdesk/rustdesk-server#env-variables
49
+RUST_LOG=info
50
+`;
51
+                    }
52
+                    this.is_content_changed = true;
53
+                }
54
+                console.warn(`${this.file} file is missing`);
55
+            }
56
+        }
57
+    }
58
+    async editorSave(editor, e) {
59
+        if (e.ctrlKey && e.keyCode === 19 && this.is_edit_mode && !this.locked) {
60
+            this.locked = true;
61
+            try {
62
+                let now = Date.now(),
63
+                    content = this.editor.doc.getValue(),
64
+                    file = await path.resolveResource(this.file);
65
+                await fs.writeTextFile(file, content);
66
+                event.emit('__action__', 'restart');
67
+            } catch (e) {
68
+                console.error(e);
69
+            } finally {
70
+                this.locked = false;
71
+            }
72
+        }
73
+    }
74
+    editorScroll(e) {
75
+        let info = this.editor.getScrollInfo(),
76
+            distance = info.height - info.top - info.clientHeight,
77
+            is_end = distance < 1;
78
+        if (this.is_auto_scroll !== is_end) {
79
+            this.is_auto_scroll = is_end;
80
+            this.is_form_changed = true;
81
+        }
82
+    }
83
+    formChange(e) {
84
+        switch (e.target.tagName.toLowerCase()) {
85
+            case 'input':
86
+                this.is_auto_scroll = e.target.checked;
87
+                break;
88
+        }
89
+    }
90
+    appAction(e) {
91
+        let [action, data] = e.payload;
92
+        switch (action) {
93
+            case 'file':
94
+                if (data === '.env') {
95
+                    this.is_edit_mode = true;
96
+                    this.file = `bin/${data}`;
97
+                } else {
98
+                    this.is_edit_mode = false;
99
+                    this.file = `logs/${data}`;
100
+                }
101
+                this.action_time = Date.now();
102
+                this.is_file_changed = true;
103
+                this.is_form_changed = true;
104
+                break;
105
+        }
106
+    }
107
+    render() {
108
+        if (this.is_form_changed) {
109
+            this.is_form_changed = false;
110
+            this.renderForm();
111
+        }
112
+        if (this.is_content_changed) {
113
+            this.is_content_changed = false;
114
+            this.renderEditor();
115
+        }
116
+        if (this.is_auto_scroll && !this.is_edit_mode) {
117
+            this.renderScrollbar();
118
+        }
119
+    }
120
+    renderForm() {
121
+        let form = this.form || document.querySelector('form'),
122
+            label = form.querySelectorAll('label'),
123
+            input = form.querySelector('input');
124
+        input.checked = this.is_auto_scroll;
125
+        if (this.is_edit_mode) {
126
+            label[0].style.display = 'none';
127
+            label[1].style.display = 'inline';
128
+        } else {
129
+            label[0].style.display = 'inline';
130
+            label[1].style.display = 'none';
131
+        }
132
+        return form;
133
+    }
134
+    renderEditor() {
135
+        let editor = this.editor || CodeMirror.fromTextArea(document.querySelector('textarea'), {
136
+            mode: { name: 'toml' },
137
+            lineNumbers: true,
138
+            autofocus: true
139
+        });
140
+        editor.setOption('readOnly', !this.is_edit_mode);
141
+        editor.doc.setValue(this.content);
142
+        editor.doc.clearHistory();
143
+        this.content = '';
144
+        editor.focus();
145
+        return editor;
146
+    }
147
+    renderScrollbar() {
148
+        let info = this.editor.getScrollInfo();
149
+        this.editor.scrollTo(info.left, info.height);
150
+    }
151
+}
152
+
153
+new View();

+ 34 - 0
ui/html/style.css

@@ -0,0 +1,34 @@
1
+body {
2
+    margin: 0;
3
+    background: #fff;
4
+}
5
+
6
+.CodeMirror {
7
+    height: calc(100vh - 20px);
8
+}
9
+
10
+form {
11
+    height: 20px;
12
+    position: fixed;
13
+    right: 0;
14
+    bottom: 0;
15
+    left: 5px;
16
+    font-size: 13px;
17
+    background: #fff;
18
+}
19
+
20
+form>label {
21
+    display: none;
22
+    vertical-align: middle;
23
+}
24
+
25
+form>label>input,
26
+form>label>p {
27
+    height: 19px;
28
+    padding: 0;
29
+    display: inline-block;
30
+    margin: 0;
31
+    vertical-align: middle;
32
+    cursor: pointer;
33
+    user-select: none;
34
+}

BIN
ui/icons/128x128.png


BIN
ui/icons/128x128@2x.png


BIN
ui/icons/32x32.png


BIN
ui/icons/Square107x107Logo.png


BIN
ui/icons/Square142x142Logo.png


BIN
ui/icons/Square150x150Logo.png


BIN
ui/icons/Square284x284Logo.png


BIN
ui/icons/Square30x30Logo.png


BIN
ui/icons/Square310x310Logo.png


BIN
ui/icons/Square44x44Logo.png


BIN
ui/icons/Square71x71Logo.png


BIN
ui/icons/Square89x89Logo.png


BIN
ui/icons/StoreLogo.png


BIN
ui/icons/icon.icns


BIN
ui/icons/icon.ico


BIN
ui/icons/icon.png


+ 108 - 0
ui/setup.nsi

@@ -0,0 +1,108 @@
1
+Unicode true
2
+
3
+####################################################################
4
+# Includes
5
+
6
+!include nsDialogs.nsh
7
+!include MUI2.nsh
8
+!include x64.nsh
9
+!include LogicLib.nsh
10
+
11
+####################################################################
12
+# File Info
13
+
14
+!define APP_NAME "RustDeskServer"
15
+!define PRODUCT_NAME "rustdesk_server"
16
+!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}"
17
+!define COPYRIGHT "Copyright © 2021"
18
+!define VERSION "1.1.7"
19
+
20
+VIProductVersion "${VERSION}.0"
21
+VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
22
+VIAddVersionKey "ProductVersion" "${VERSION}"
23
+VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}"
24
+VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
25
+VIAddVersionKey "FileVersion" "${VERSION}"
26
+
27
+####################################################################
28
+# Installer Attributes
29
+
30
+Name "${APP_NAME}"
31
+Outfile "${APP_NAME}.Setup.exe"
32
+Caption "Setup - ${APP_NAME}"
33
+BrandingText "${APP_NAME}"
34
+
35
+ShowInstDetails show
36
+RequestExecutionLevel admin
37
+SetOverwrite on
38
+ 
39
+InstallDir "$PROGRAMFILES64\${APP_NAME}"
40
+
41
+####################################################################
42
+# Pages
43
+
44
+!define MUI_ICON "icons\icon.ico"
45
+!define MUI_ABORTWARNING
46
+!define MUI_LANGDLL_ALLLANGUAGES
47
+!define MUI_FINISHPAGE_SHOWREADME ""
48
+!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Startup Shortcut"
49
+!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateStartupShortcut
50
+!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe"
51
+
52
+!insertmacro MUI_PAGE_DIRECTORY
53
+!insertmacro MUI_PAGE_INSTFILES
54
+!insertmacro MUI_PAGE_FINISH
55
+
56
+####################################################################
57
+# Language
58
+
59
+!insertmacro MUI_LANGUAGE "English"
60
+!insertmacro MUI_LANGUAGE "SimpChinese"
61
+
62
+####################################################################
63
+# Sections
64
+
65
+Section "Install"
66
+  SetShellVarContext all
67
+  nsExec::Exec 'sc stop hbbr'
68
+  nsExec::Exec 'sc stop hbbs'
69
+  nsExec::Exec 'taskkill /F /IM ${PRODUCT_NAME}.exe'
70
+  Sleep 500 ;
71
+
72
+  SetOutPath $INSTDIR
73
+  File /r "setup\*.*"
74
+  WriteUninstaller $INSTDIR\uninstall.exe
75
+
76
+  CreateDirectory "$SMPROGRAMS\${APP_NAME}"
77
+  CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
78
+  CreateShortCut "$SMPROGRAMS\${APP_NAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
79
+  CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
80
+  CreateShortCut "$SMSTARTUP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
81
+
82
+  nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=in action=allow program="$INSTDIR\hbbs.exe" enable=yes'
83
+  nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\hbbs.exe" enable=yes'
84
+  nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=in action=allow program="$INSTDIR\hbbr.exe" enable=yes'
85
+  nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\hbbr.exe" enable=yes'
86
+SectionEnd
87
+
88
+Section "Uninstall"
89
+  SetShellVarContext all
90
+  nsExec::Exec 'sc stop hbbr'
91
+  nsExec::Exec 'sc stop hbbs'
92
+  nsExec::Exec 'taskkill /F /IM ${PRODUCT_NAME}.exe'
93
+  Sleep 500 ;
94
+
95
+  RMDir /r "$SMPROGRAMS\${APP_NAME}"
96
+  Delete "$SMSTARTUP\${APP_NAME}.lnk"
97
+  Delete "$DESKTOP\${APP_NAME}.lnk"
98
+  nsExec::Exec 'sc delete hbbr'
99
+  nsExec::Exec 'sc delete hbbs'
100
+  nsExec::Exec 'netsh advfirewall firewall delete rule name="${APP_NAME}"'
101
+SectionEnd
102
+
103
+####################################################################
104
+# Functions
105
+
106
+Function CreateStartupShortcut
107
+  CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
108
+FunctionEnd

BIN
ui/setup/service/nssm.exe


+ 23 - 0
ui/setup/service/run.cmd

@@ -0,0 +1,23 @@
1
+@echo off
2
+%~d0
3
+cd "%~dp0"
4
+set nssm="%cd%\nssm"
5
+cd ..
6
+
7
+%nssm% install %1 "%cd%\bin\%1.exe"
8
+
9
+%nssm% set %1 DisplayName %1
10
+%nssm% set %1 Description rustdesk %1 server
11
+%nssm% set %1 Start SERVICE_AUTO_START
12
+
13
+%nssm% set %1 ObjectName LocalSystem
14
+%nssm% set %1 Type SERVICE_WIN32_OWN_PROCESS
15
+
16
+%nssm% set %1 AppThrottle 1000
17
+%nssm% set %1 AppExit Default Restart
18
+%nssm% set %1 AppRestartDelay 0
19
+
20
+%nssm% set %1 AppStdout "%cd%\logs\%1.out"
21
+%nssm% set %1 AppStderr "%cd%\logs\%1.err"
22
+
23
+%nssm% start %1

+ 5 - 0
ui/src/adapter/mod.rs

@@ -0,0 +1,5 @@
1
+pub mod view;
2
+pub mod service;
3
+
4
+pub use view::*;
5
+pub use service::*;

+ 3 - 0
ui/src/adapter/service/mod.rs

@@ -0,0 +1,3 @@
1
+pub mod windows;
2
+
3
+pub use windows::*;

+ 130 - 0
ui/src/adapter/service/windows.rs

@@ -0,0 +1,130 @@
1
+use std::{ffi::OsStr, process::Command};
2
+
3
+use crate::{path, usecase::service::*};
4
+use derive_new::new;
5
+use windows_service::{
6
+    service::ServiceAccess,
7
+    service_manager::{ServiceManager, ServiceManagerAccess},
8
+};
9
+
10
+#[derive(Debug, new)]
11
+pub struct WindowsDesktopService {
12
+    #[new(value = "DesktopServiceState::Stopped")]
13
+    pub state: DesktopServiceState,
14
+}
15
+
16
+impl IDesktopService for WindowsDesktopService {
17
+    fn start(&mut self) {
18
+        call(
19
+            [
20
+                "echo.",
21
+                "%nssm% stop hbbr",
22
+                "%nssm% remove hbbr confirm",
23
+                "%nssm% stop hbbs",
24
+                "%nssm% remove hbbs confirm",
25
+                "mkdir logs",
26
+                "echo.",
27
+                "service\\run.cmd hbbs",
28
+                "echo.",
29
+                "service\\run.cmd hbbr",
30
+                "echo.",
31
+                "@ping 127.1 -n 3 >nul",
32
+            ]
33
+            .join(" & "),
34
+        );
35
+        self.check();
36
+    }
37
+    fn stop(&mut self) {
38
+        call(
39
+            [
40
+                "echo.",
41
+                "%nssm% stop hbbr",
42
+                "%nssm% remove hbbr confirm",
43
+                "echo.",
44
+                "%nssm% stop hbbs",
45
+                "%nssm% remove hbbs confirm",
46
+                "echo.",
47
+                "@ping 127.1 -n 3 >nul",
48
+            ]
49
+            .join(" & "),
50
+        );
51
+        self.check();
52
+    }
53
+    fn restart(&mut self) {
54
+        nssm(["restart", "hbbs"].map(|x| x.to_owned()));
55
+        nssm(["restart", "hbbr"].map(|x| x.to_owned()));
56
+        self.check();
57
+    }
58
+    fn pause(&mut self) {
59
+        call(
60
+            [
61
+                "echo.",
62
+                "%nssm% stop hbbr",
63
+                "echo.",
64
+                "%nssm% stop hbbs",
65
+                "echo.",
66
+                "@ping 127.1 -n 3 >nul",
67
+            ]
68
+            .join(" & "),
69
+        );
70
+        self.check();
71
+    }
72
+    fn check(&mut self) -> DesktopServiceState {
73
+        self.state = match service_status("hbbs").as_str() {
74
+            "Running" => DesktopServiceState::Started,
75
+            // "Stopped" => DeskServerServiceState::Paused,
76
+            _ => DesktopServiceState::Stopped,
77
+        };
78
+        self.state.to_owned()
79
+    }
80
+}
81
+
82
+fn call(cmd: String) {
83
+    Command::new("cmd")
84
+        .current_dir(&path())
85
+        .env("nssm", "service\\nssm.exe")
86
+        .arg("/c")
87
+        .arg("start")
88
+        .arg("cmd")
89
+        .arg("/c")
90
+        .arg(cmd)
91
+        .output()
92
+        .expect("cmd exec error!");
93
+}
94
+
95
+fn exec<I, S>(program: S, args: I) -> String
96
+where
97
+    I: IntoIterator<Item = S>,
98
+    S: AsRef<OsStr>,
99
+{
100
+    match Command::new(program).args(args).output() {
101
+        Ok(out) => String::from_utf8(out.stdout).unwrap_or("".to_owned()),
102
+        Err(e) => e.to_string(),
103
+    }
104
+}
105
+
106
+fn nssm<I>(args: I) -> String
107
+where
108
+    I: IntoIterator<Item = String>,
109
+{
110
+    exec(
111
+        format!("{}\\service\\nssm.exe", path().to_str().unwrap_or_default()),
112
+        args,
113
+    )
114
+    .replace("\0", "")
115
+    .trim()
116
+    .to_owned()
117
+}
118
+
119
+fn service_status(name: &str) -> String {
120
+    match ServiceManager::local_computer(None::<&OsStr>, ServiceManagerAccess::CONNECT) {
121
+        Ok(manager) => match manager.open_service(name, ServiceAccess::QUERY_STATUS) {
122
+            Ok(service) => match service.query_status() {
123
+                Ok(status) => format!("{:?}", status.current_state),
124
+                Err(e) => e.to_string(),
125
+            },
126
+            Err(e) => e.to_string(),
127
+        },
128
+        Err(e) => e.to_string(),
129
+    }
130
+}

+ 220 - 0
ui/src/adapter/view/desktop.rs

@@ -0,0 +1,220 @@
1
+use std::{
2
+    process::exit,
3
+    time::{Duration, Instant},
4
+};
5
+
6
+use crate::{
7
+    path,
8
+    usecase::{view::Event, DesktopServiceState},
9
+    BUFFER,
10
+};
11
+use async_std::task::sleep;
12
+use crossbeam_channel::{Receiver, Sender};
13
+use tauri::{
14
+    CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu,
15
+    SystemTrayMenuItem, WindowEvent,
16
+};
17
+
18
+pub async fn run(sender: Sender<Event>, receiver: Receiver<Event>) {
19
+    let setup_sender = sender.clone();
20
+    let menu_sender = sender.clone();
21
+    let tray_sender = sender.clone();
22
+    let menu = Menu::new()
23
+        .add_submenu(Submenu::new(
24
+            "Service",
25
+            Menu::new()
26
+                .add_item(CustomMenuItem::new("restart", "Restart"))
27
+                .add_native_item(MenuItem::Separator)
28
+                .add_item(CustomMenuItem::new("start", "Start"))
29
+                .add_item(CustomMenuItem::new("stop", "Stop")),
30
+        ))
31
+        .add_submenu(Submenu::new(
32
+            "Logs",
33
+            Menu::new()
34
+                .add_item(CustomMenuItem::new("hbbs.out", "hbbs.out"))
35
+                .add_item(CustomMenuItem::new("hbbs.err", "hbbs.err"))
36
+                .add_native_item(MenuItem::Separator)
37
+                .add_item(CustomMenuItem::new("hbbr.out", "hbbr.out"))
38
+                .add_item(CustomMenuItem::new("hbbr.err", "hbbr.err")),
39
+        ))
40
+        .add_submenu(Submenu::new(
41
+            "Configuration",
42
+            Menu::new().add_item(CustomMenuItem::new(".env", ".env")),
43
+        ));
44
+    let tray = SystemTray::new().with_menu(
45
+        SystemTrayMenu::new()
46
+            .add_item(CustomMenuItem::new("restart", "Restart"))
47
+            .add_native_item(SystemTrayMenuItem::Separator)
48
+            .add_item(CustomMenuItem::new("start", "Start"))
49
+            .add_item(CustomMenuItem::new("stop", "Stop"))
50
+            .add_native_item(SystemTrayMenuItem::Separator)
51
+            .add_item(CustomMenuItem::new("exit", "Exit GUI")),
52
+    );
53
+    let mut app = tauri::Builder::default()
54
+        .on_window_event(|event| match event.event() {
55
+            // WindowEvent::Resized(size) => {
56
+            //     if size.width == 0 && size.height == 0 {
57
+            //         event.window().hide().unwrap();
58
+            //     }
59
+            // }
60
+            WindowEvent::CloseRequested { api, .. } => {
61
+                api.prevent_close();
62
+                event.window().hide().unwrap();
63
+            }
64
+            _ => {}
65
+        })
66
+        .menu(menu)
67
+        .on_menu_event(move |event| {
68
+            // println!(
69
+            //     "send {}: {}",
70
+            //     std::time::SystemTime::now()
71
+            //         .duration_since(std::time::UNIX_EPOCH)
72
+            //         .unwrap_or_default()
73
+            //         .as_millis(),
74
+            //     event.menu_item_id()
75
+            // );
76
+            menu_sender
77
+                .send(Event::ViewAction(event.menu_item_id().to_owned()))
78
+                .unwrap_or_default()
79
+        })
80
+        .system_tray(tray)
81
+        .on_system_tray_event(move |app, event| match event {
82
+            SystemTrayEvent::LeftClick { .. } => {
83
+                let main = app.get_window("main").unwrap();
84
+                if main.is_visible().unwrap() {
85
+                    main.hide().unwrap();
86
+                } else {
87
+                    main.show().unwrap();
88
+                    main.unminimize().unwrap();
89
+                    main.set_focus().unwrap();
90
+                }
91
+            }
92
+            SystemTrayEvent::MenuItemClick { id, .. } => {
93
+                tray_sender.send(Event::ViewAction(id)).unwrap_or_default();
94
+            }
95
+            _ => {}
96
+        })
97
+        .setup(move |app| {
98
+            setup_sender.send(Event::ViewInit).unwrap_or_default();
99
+            app.listen_global("__action__", move |msg| {
100
+                match msg.payload().unwrap_or_default() {
101
+                    r#""__init__""# => setup_sender.send(Event::BroswerInit).unwrap_or_default(),
102
+                    r#""restart""# => setup_sender
103
+                        .send(Event::BrowserAction("restart".to_owned()))
104
+                        .unwrap_or_default(),
105
+                    _ => (),
106
+                }
107
+            });
108
+            Ok(())
109
+        })
110
+        .invoke_handler(tauri::generate_handler![root])
111
+        .build(tauri::generate_context!())
112
+        .expect("error while running tauri application");
113
+    let mut now = Instant::now();
114
+    let mut blink = false;
115
+    let mut span = 0;
116
+    let mut title = "".to_owned();
117
+    let product = "RustDesk Server";
118
+    let buffer = BUFFER.get().unwrap().to_owned();
119
+    loop {
120
+        for _ in 1..buffer {
121
+            match receiver.recv_timeout(Duration::from_nanos(1)) {
122
+                Ok(event) => {
123
+                    let main = app.get_window("main").unwrap();
124
+                    let menu = main.menu_handle();
125
+                    let tray = app.tray_handle();
126
+                    match event {
127
+                        Event::BrowserUpdate((action, data)) => match action.as_str() {
128
+                            "file" => {
129
+                                let list = ["hbbs.out", "hbbs.err", "hbbr.out", "hbbr.err", ".env"];
130
+                                let id = data.as_str();
131
+                                if list.contains(&id) {
132
+                                    for file in list {
133
+                                        menu.get_item(file)
134
+                                            .set_selected(file == id)
135
+                                            .unwrap_or_default();
136
+                                    }
137
+                                    // println!(
138
+                                    //     "emit {}: {}",
139
+                                    //     std::time::SystemTime::now()
140
+                                    //         .duration_since(std::time::UNIX_EPOCH)
141
+                                    //         .unwrap_or_default()
142
+                                    //         .as_millis(),
143
+                                    //     data
144
+                                    // );
145
+                                    app.emit_all("__update__", (action, data))
146
+                                        .unwrap_or_default();
147
+                                }
148
+                            }
149
+                            _ => (),
150
+                        },
151
+                        Event::ViewRenderAppExit => exit(0),
152
+                        Event::ViewRenderServiceState(state) => {
153
+                            let enabled = |id, enabled| {
154
+                                menu.get_item(id).set_enabled(enabled).unwrap_or_default();
155
+                                tray.get_item(id).set_enabled(enabled).unwrap_or_default();
156
+                            };
157
+                            title = format!("{} {:?}", product, state);
158
+                            main.set_title(title.as_str()).unwrap_or_default();
159
+                            match state {
160
+                                DesktopServiceState::Started => {
161
+                                    enabled("start", false);
162
+                                    enabled("stop", true);
163
+                                    enabled("restart", true);
164
+                                    blink = false;
165
+                                }
166
+                                DesktopServiceState::Stopped => {
167
+                                    enabled("start", true);
168
+                                    enabled("stop", false);
169
+                                    enabled("restart", false);
170
+                                    blink = true;
171
+                                }
172
+                                _ => {
173
+                                    enabled("start", false);
174
+                                    enabled("stop", false);
175
+                                    enabled("restart", false);
176
+                                    blink = true;
177
+                                }
178
+                            }
179
+                        }
180
+                        _ => (),
181
+                    }
182
+                }
183
+                Err(_) => break,
184
+            }
185
+        }
186
+        let elapsed = now.elapsed().as_micros();
187
+        if elapsed > 16666 {
188
+            now = Instant::now();
189
+            // println!("{}ms", elapsed as f64 * 0.001);
190
+            let iteration = app.run_iteration();
191
+            if iteration.window_count == 0 {
192
+                break;
193
+            }
194
+            if blink {
195
+                if span > 1000000 {
196
+                    span = 0;
197
+                    app.get_window("main")
198
+                        .unwrap()
199
+                        .set_title(title.as_str())
200
+                        .unwrap_or_default();
201
+                } else {
202
+                    span += elapsed;
203
+                    if span > 500000 {
204
+                        app.get_window("main")
205
+                            .unwrap()
206
+                            .set_title(product)
207
+                            .unwrap_or_default();
208
+                    }
209
+                }
210
+            }
211
+        } else {
212
+            sleep(Duration::from_micros(999)).await;
213
+        }
214
+    }
215
+}
216
+
217
+#[tauri::command]
218
+fn root() -> String {
219
+    path().to_str().unwrap_or_default().to_owned()
220
+}

+ 3 - 0
ui/src/adapter/view/mod.rs

@@ -0,0 +1,3 @@
1
+pub mod desktop;
2
+
3
+pub use desktop::*;

+ 17 - 0
ui/src/lib.rs

@@ -0,0 +1,17 @@
1
+use std::{env::current_exe, path::PathBuf};
2
+
3
+use once_cell::sync::OnceCell;
4
+
5
+pub mod adapter;
6
+pub mod usecase;
7
+
8
+pub static BUFFER: OnceCell<usize> = OnceCell::new();
9
+
10
+pub fn path() -> PathBuf {
11
+    current_exe()
12
+        .unwrap_or_default()
13
+        .as_path()
14
+        .parent()
15
+        .unwrap()
16
+        .to_owned()
17
+}

+ 25 - 0
ui/src/main.rs

@@ -0,0 +1,25 @@
1
+#![cfg_attr(
2
+    all(not(debug_assertions), target_os = "windows"),
3
+    windows_subsystem = "windows"
4
+)]
5
+
6
+use async_std::{
7
+    prelude::FutureExt,
8
+    task::{spawn, spawn_local},
9
+};
10
+use crossbeam_channel::bounded;
11
+use rustdesk_server::{
12
+    usecase::{presenter, view, watcher},
13
+    BUFFER,
14
+};
15
+
16
+#[async_std::main]
17
+async fn main() {
18
+    let buffer = BUFFER.get_or_init(|| 10).to_owned();
19
+    let (view_sender, presenter_receiver) = bounded(buffer);
20
+    let (presenter_sender, view_receiver) = bounded(buffer);
21
+    spawn_local(view::create(presenter_sender.clone(), presenter_receiver))
22
+        .join(spawn(presenter::create(view_sender, view_receiver)))
23
+        .join(spawn(watcher::create(presenter_sender)))
24
+        .await;
25
+}

+ 9 - 0
ui/src/usecase/mod.rs

@@ -0,0 +1,9 @@
1
+pub mod presenter;
2
+pub mod service;
3
+pub mod view;
4
+pub mod watcher;
5
+
6
+pub use presenter::*;
7
+pub use service::*;
8
+pub use view::*;
9
+pub use watcher::*;

+ 59 - 0
ui/src/usecase/presenter.rs

@@ -0,0 +1,59 @@
1
+use std::time::{Duration, Instant};
2
+
3
+use super::{service, DesktopServiceState, Event};
4
+use crate::BUFFER;
5
+use async_std::task::sleep;
6
+use crossbeam_channel::{Receiver, Sender};
7
+
8
+pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
9
+    let mut now = Instant::now();
10
+    let buffer = BUFFER.get().unwrap().to_owned();
11
+    let send = |event| sender.send(event).unwrap_or_default();
12
+    if let Some(mut service) = service::create() {
13
+        let mut service_state = DesktopServiceState::Unknown;
14
+        let mut file = "hbbs.out".to_owned();
15
+        send(Event::ViewRenderServiceState(service_state.to_owned()));
16
+        loop {
17
+            for _ in 1..buffer {
18
+                match receiver.recv_timeout(Duration::from_nanos(1)) {
19
+                    Ok(event) => match event {
20
+                        Event::BroswerInit => {
21
+                            send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
22
+                        }
23
+                        Event::BrowserAction(action) => match action.as_str() {
24
+                            "restart" => service.restart(),
25
+                            _ => (),
26
+                        },
27
+                        Event::FileChange(path) => {
28
+                            if path == file {
29
+                                send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
30
+                            }
31
+                        }
32
+                        Event::ViewAction(action) => match action.as_str() {
33
+                            "start" => service.start(),
34
+                            "stop" => service.stop(),
35
+                            "restart" => service.restart(),
36
+                            "pause" => service.pause(),
37
+                            "exit" => send(Event::ViewRenderAppExit),
38
+                            _ => {
39
+                                file = action;
40
+                                send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
41
+                            }
42
+                        },
43
+                        _ => (),
44
+                    },
45
+                    Err(_) => break,
46
+                }
47
+            }
48
+            sleep(Duration::from_micros(999)).await;
49
+            if now.elapsed().as_millis() > 999 {
50
+                let state = service.check();
51
+                if state != service_state {
52
+                    service_state = state.to_owned();
53
+                    send(Event::ViewRenderServiceState(state));
54
+                }
55
+                now = Instant::now();
56
+            }
57
+        }
58
+    }
59
+}

+ 24 - 0
ui/src/usecase/service.rs

@@ -0,0 +1,24 @@
1
+use crate::adapter;
2
+
3
+pub fn create() -> Option<Box<dyn IDesktopService + Send>> {
4
+    if cfg!(target_os = "windows") {
5
+        return Some(Box::new(adapter::WindowsDesktopService::new()));
6
+    }
7
+    None
8
+}
9
+
10
+#[derive(Debug, Clone, PartialEq)]
11
+pub enum DesktopServiceState {
12
+    Paused,
13
+    Started,
14
+    Stopped,
15
+    Unknown,
16
+}
17
+
18
+pub trait IDesktopService {
19
+    fn start(&mut self);
20
+    fn stop(&mut self);
21
+    fn restart(&mut self);
22
+    fn pause(&mut self);
23
+    fn check(&mut self) -> DesktopServiceState;
24
+}

+ 22 - 0
ui/src/usecase/view.rs

@@ -0,0 +1,22 @@
1
+use super::DesktopServiceState;
2
+use crate::adapter::desktop;
3
+use crossbeam_channel::{Receiver, Sender};
4
+
5
+pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
6
+    desktop::run(sender, receiver).await;
7
+}
8
+
9
+#[derive(Debug, Clone, PartialEq)]
10
+pub enum Event {
11
+    BrowserAction(String),
12
+    BroswerInit,
13
+    BrowserUpdate((String, String)),
14
+    BrowserRender(String),
15
+    FileChange(String),
16
+    ViewAction(String),
17
+    ViewInit,
18
+    ViewUpdate(String),
19
+    ViewRender(String),
20
+    ViewRenderAppExit,
21
+    ViewRenderServiceState(DesktopServiceState),
22
+}

+ 46 - 0
ui/src/usecase/watcher.rs

@@ -0,0 +1,46 @@
1
+use std::{path::Path, time::Duration};
2
+
3
+use super::Event;
4
+use crate::path;
5
+use async_std::task::{sleep, spawn_blocking};
6
+use crossbeam_channel::{bounded, Sender};
7
+use notify::{Config, RecommendedWatcher, RecursiveMode, Result, Watcher};
8
+
9
+pub async fn create(sender: Sender<Event>) {
10
+    loop {
11
+        let watch_sender = sender.clone();
12
+        match spawn_blocking(|| {
13
+            watch(
14
+                format!("{}/logs/", path().to_str().unwrap_or_default()),
15
+                watch_sender,
16
+            )
17
+        })
18
+        .await
19
+        {
20
+            Ok(_) => (),
21
+            Err(e) => println!("error: {e}"),
22
+        }
23
+        sleep(Duration::from_secs(1)).await;
24
+    }
25
+}
26
+
27
+fn watch<P: AsRef<Path>>(path: P, sender: Sender<Event>) -> Result<()> {
28
+    let (tx, rx) = bounded(10);
29
+    let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
30
+    watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
31
+    for res in rx {
32
+        let event = res?;
33
+        for p in event.paths {
34
+            let path = p
35
+                .file_name()
36
+                .unwrap_or_default()
37
+                .to_str()
38
+                .unwrap_or_default()
39
+                .to_owned();
40
+            if path.len() > 0 {
41
+                sender.send(Event::FileChange(path)).unwrap_or_default();
42
+            }
43
+        }
44
+    }
45
+    Ok(())
46
+}

+ 89 - 0
ui/tauri.conf.json

@@ -0,0 +1,89 @@
1
+{
2
+  "build": {
3
+    "beforeDevCommand": "",
4
+    "beforeBuildCommand": "",
5
+    "devPath": "html",
6
+    "distDir": "html",
7
+    "withGlobalTauri": true
8
+  },
9
+  "package": {
10
+    "productName": "rustdesk_server",
11
+    "version": "0.1.1"
12
+  },
13
+  "tauri": {
14
+    "allowlist": {
15
+      "all": false,
16
+      "fs": {
17
+        "scope": [
18
+          "$RESOURCE/bin/.env",
19
+          "$RESOURCE/logs/*"
20
+        ],
21
+        "all": false,
22
+        "exists": true,
23
+        "readDir": true,
24
+        "readFile": true,
25
+        "writeFile": true
26
+      },
27
+      "path": {
28
+        "all": true
29
+      },
30
+      "shell": {
31
+        "all": false,
32
+        "open": true
33
+      }
34
+    },
35
+    "bundle": {
36
+      "active": true,
37
+      "category": "DeveloperTool",
38
+      "copyright": "",
39
+      "deb": {
40
+        "depends": []
41
+      },
42
+      "externalBin": [],
43
+      "icon": [
44
+        "icons/32x32.png",
45
+        "icons/128x128.png",
46
+        "icons/128x128@2x.png",
47
+        "icons/icon.icns",
48
+        "icons/icon.ico"
49
+      ],
50
+      "identifier": "rustdesk_server",
51
+      "longDescription": "",
52
+      "macOS": {
53
+        "entitlements": null,
54
+        "exceptionDomain": "",
55
+        "frameworks": [],
56
+        "providerShortName": null,
57
+        "signingIdentity": null
58
+      },
59
+      "resources": [],
60
+      "shortDescription": "",
61
+      "targets": "all",
62
+      "windows": {
63
+        "certificateThumbprint": null,
64
+        "digestAlgorithm": "sha256",
65
+        "timestampUrl": ""
66
+      }
67
+    },
68
+    "security": {
69
+      "csp": null
70
+    },
71
+    "systemTray": {
72
+      "iconPath": "icons/icon.ico",
73
+      "iconAsTemplate": true
74
+    },
75
+    "updater": {
76
+      "active": false
77
+    },
78
+    "windows": [
79
+      {
80
+        "center": true,
81
+        "fullscreen": false,
82
+        "height": 600,
83
+        "resizable": true,
84
+        "title": "RustDesk Server",
85
+        "width": 980
86
+      }
87
+    ]
88
+  }
89
+}