UI
22
.github/workflows/build.yaml
vendored
@ -95,7 +95,24 @@ jobs:
|
||||
with:
|
||||
command: build
|
||||
args: --release --all-features --target=x86_64-pc-windows-msvc
|
||||
use-cross: true
|
||||
use-cross: true
|
||||
|
||||
- name: Install NSIS
|
||||
run: |
|
||||
iwr -useb get.scoop.sh -outfile 'install.ps1'
|
||||
.\install.ps1 -RunAsAdmin
|
||||
scoop update
|
||||
scoop bucket add extras
|
||||
scoop install nsis
|
||||
|
||||
- run: rustup default nightly
|
||||
- run: cargo build --release
|
||||
working-directory: ./ui
|
||||
- run: xcopy /y target\x86_64-pc-windows-msvc\release\*.exe ui\setup\bin\
|
||||
- run: xcopy /y ui\target\release\*.exe ui\setup\
|
||||
- run: mkdir ui\setup\logs
|
||||
- run: makensis /V1 setup.nsi
|
||||
working-directory: ./ui
|
||||
|
||||
- name: Publish Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
@ -104,7 +121,8 @@ jobs:
|
||||
path: |
|
||||
target\x86_64-pc-windows-msvc\release\hbbr.exe
|
||||
target\x86_64-pc-windows-msvc\release\hbbs.exe
|
||||
target\x86_64-pc-windows-msvc\release\rustdesk-utils.exe
|
||||
target\x86_64-pc-windows-msvc\release\rustdesk-utils.exe
|
||||
ui\RustDeskServer.Setup.exe
|
||||
if-no-files-found: error
|
||||
|
||||
# github (draft) release with all binaries
|
||||
|
@ -57,3 +57,4 @@ hbb_common = { path = "libs/hbb_common" }
|
||||
|
||||
[workspace]
|
||||
members = ["libs/hbb_common"]
|
||||
exclude = ["ui"]
|
||||
|
4
ui/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
3787
ui/Cargo.lock
generated
Normal file
31
ui/Cargo.toml
Normal file
@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "rustdesk_server"
|
||||
version = "0.1.1"
|
||||
description = "rustdesk server gui"
|
||||
authors = ["elilchen"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
winres = "0.1"
|
||||
|
||||
[dependencies]
|
||||
async-std = { version = "1.12", features = ["attributes", "unstable"] }
|
||||
crossbeam-channel = "0.5"
|
||||
derive-new = "0.5"
|
||||
notify = "5.1"
|
||||
once_cell = "1.17"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2", features = ["fs-exists", "fs-read-dir", "fs-read-file", "fs-write-file", "path-all", "shell-open", "system-tray"] }
|
||||
windows-service = "0.5.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = ["custom-protocol"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
21
ui/build.rs
Normal file
@ -0,0 +1,21 @@
|
||||
fn main() {
|
||||
tauri_build::build();
|
||||
if cfg!(target_os = "windows") {
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("icons\\icon.ico");
|
||||
res.set_manifest(
|
||||
r#"
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
"#,
|
||||
);
|
||||
res.compile().unwrap();
|
||||
}
|
||||
}
|
344
ui/html/editor/codemirror.css
Normal file
@ -0,0 +1,344 @@
|
||||
/* BASICS */
|
||||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
color: black;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
}
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like {
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
background-color: white; /* The little square between H and V scrollbars */
|
||||
}
|
||||
|
||||
/* GUTTER */
|
||||
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.CodeMirror-linenumbers {}
|
||||
.CodeMirror-linenumber {
|
||||
padding: 0 3px 0 5px;
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.CodeMirror-guttermarker { color: black; }
|
||||
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||
|
||||
/* CURSOR */
|
||||
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
}
|
||||
/* Shown when moving in bi-directional text */
|
||||
.CodeMirror div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
.cm-fat-cursor .CodeMirror-cursor {
|
||||
width: auto;
|
||||
border: 0 !important;
|
||||
background: #7e7;
|
||||
}
|
||||
.cm-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
.cm-fat-cursor .CodeMirror-line::selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span::selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
|
||||
.cm-fat-cursor .CodeMirror-line::-moz-selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
|
||||
.cm-fat-cursor { caret-color: transparent; }
|
||||
@-moz-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
|
||||
/* Can style cursor different in overwrite (non-insert) mode */
|
||||
.CodeMirror-overwrite .CodeMirror-cursor {}
|
||||
|
||||
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||
|
||||
.CodeMirror-rulers {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: -50px; bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.CodeMirror-ruler {
|
||||
border-left: 1px solid #ccc;
|
||||
top: 0; bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* DEFAULT THEME */
|
||||
|
||||
.cm-s-default .cm-header {color: blue;}
|
||||
.cm-s-default .cm-quote {color: #090;}
|
||||
.cm-negative {color: #d44;}
|
||||
.cm-positive {color: #292;}
|
||||
.cm-header, .cm-strong {font-weight: bold;}
|
||||
.cm-em {font-style: italic;}
|
||||
.cm-link {text-decoration: underline;}
|
||||
.cm-strikethrough {text-decoration: line-through;}
|
||||
|
||||
.cm-s-default .cm-keyword {color: #708;}
|
||||
.cm-s-default .cm-atom {color: #219;}
|
||||
.cm-s-default .cm-number {color: #164;}
|
||||
.cm-s-default .cm-def {color: #00f;}
|
||||
.cm-s-default .cm-variable,
|
||||
.cm-s-default .cm-punctuation,
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-operator {}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
.cm-s-default .cm-meta {color: #555;}
|
||||
.cm-s-default .cm-qualifier {color: #555;}
|
||||
.cm-s-default .cm-builtin {color: #30a;}
|
||||
.cm-s-default .cm-bracket {color: #997;}
|
||||
.cm-s-default .cm-tag {color: #170;}
|
||||
.cm-s-default .cm-attribute {color: #00c;}
|
||||
.cm-s-default .cm-hr {color: #999;}
|
||||
.cm-s-default .cm-link {color: #00c;}
|
||||
|
||||
.cm-s-default .cm-error {color: #f00;}
|
||||
.cm-invalidchar {color: #f00;}
|
||||
|
||||
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/* The rest of this file contains styles related to the mechanics of
|
||||
the editor. You probably shouldn't touch them. */
|
||||
|
||||
.CodeMirror {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: scroll !important; /* Things will break if this is overridden */
|
||||
/* 50px is the magic margin used to hide the element's real scrollbars */
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-bottom: -50px; margin-right: -50px;
|
||||
padding-bottom: 50px;
|
||||
height: 100%;
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 50px solid transparent;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
before actual scrolling happens, thus preventing shaking and
|
||||
flickering artifacts. */
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
outline: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
right: 0; top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 0; left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
right: 0; bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutter-filler {
|
||||
left: 0; bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
position: absolute; left: 0; top: 0;
|
||||
min-height: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
white-space: normal;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: -50px;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.CodeMirror-gutter-background {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
|
||||
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: text;
|
||||
min-height: 1px; /* prevents collapsing before first draw */
|
||||
}
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like {
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-font-variant-ligatures: contextual;
|
||||
font-variant-ligatures: contextual;
|
||||
}
|
||||
.CodeMirror-wrap pre.CodeMirror-line,
|
||||
.CodeMirror-wrap pre.CodeMirror-line-like {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.CodeMirror-linebackground {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-linewidget {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0.1px; /* Force widget margins to stay inside of the container */
|
||||
}
|
||||
|
||||
.CodeMirror-widget {}
|
||||
|
||||
.CodeMirror-rtl pre { direction: rtl; }
|
||||
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Force content-box sizing for the elements where we expect it */
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-sizer,
|
||||
.CodeMirror-gutter,
|
||||
.CodeMirror-gutters,
|
||||
.CodeMirror-linenumber {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.CodeMirror-measure pre { position: static; }
|
||||
|
||||
div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
div.CodeMirror-dragcursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-focused div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-selected { background: #d9d9d9; }
|
||||
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||
.CodeMirror-crosshair { cursor: crosshair; }
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||
|
||||
.cm-searching {
|
||||
background-color: #ffa;
|
||||
background-color: rgba(255, 255, 0, .4);
|
||||
}
|
||||
|
||||
/* Used to force a border model for a node */
|
||||
.cm-force-border { padding-right: .1px; }
|
||||
|
||||
@media print {
|
||||
/* Hide the cursor when printing */
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* See issue #2901 */
|
||||
.cm-tab-wrap-hack:after { content: ''; }
|
||||
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext { background: none; }
|
9874
ui/html/editor/codemirror.js
Normal file
88
ui/html/editor/toml.js
Normal file
@ -0,0 +1,88 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineMode("toml", function () {
|
||||
return {
|
||||
startState: function () {
|
||||
return {
|
||||
inString: false,
|
||||
stringType: "",
|
||||
lhs: true,
|
||||
inArray: 0
|
||||
};
|
||||
},
|
||||
token: function (stream, state) {
|
||||
//check for state changes
|
||||
if (!state.inString && ((stream.peek() == '"') || (stream.peek() == "'"))) {
|
||||
state.stringType = stream.peek();
|
||||
stream.next(); // Skip quote
|
||||
state.inString = true; // Update state
|
||||
}
|
||||
if (stream.sol() && state.inArray === 0) {
|
||||
state.lhs = true;
|
||||
}
|
||||
//return state
|
||||
if (state.inString) {
|
||||
while (state.inString && !stream.eol()) {
|
||||
if (stream.peek() === state.stringType) {
|
||||
stream.next(); // Skip quote
|
||||
state.inString = false; // Clear flag
|
||||
} else if (stream.peek() === '\\') {
|
||||
stream.next();
|
||||
stream.next();
|
||||
} else {
|
||||
stream.match(/^.[^\\\"\']*/);
|
||||
}
|
||||
}
|
||||
return state.lhs ? "property string" : "string"; // Token style
|
||||
} else if (state.inArray && stream.peek() === ']') {
|
||||
stream.next();
|
||||
state.inArray--;
|
||||
return 'bracket';
|
||||
} else if (state.lhs && stream.peek() === '[' && stream.skipTo(']')) {
|
||||
stream.next();//skip closing ]
|
||||
// array of objects has an extra open & close []
|
||||
if (stream.peek() === ']') stream.next();
|
||||
return "atom";
|
||||
} else if (stream.peek() === "#") {
|
||||
stream.skipToEnd();
|
||||
return "comment";
|
||||
} else if (stream.eatSpace()) {
|
||||
return null;
|
||||
} else if (state.lhs && stream.eatWhile(function (c) { return c != '=' && c != ' '; })) {
|
||||
return "property";
|
||||
} else if (state.lhs && stream.peek() === "=") {
|
||||
stream.next();
|
||||
state.lhs = false;
|
||||
return null;
|
||||
} else if (!state.lhs && stream.match(/^\d\d\d\d[\d\-\:\.T]*Z/)) {
|
||||
return 'atom'; //date
|
||||
} else if (!state.lhs && (stream.match('true') || stream.match('false'))) {
|
||||
return 'atom';
|
||||
} else if (!state.lhs && stream.peek() === '[') {
|
||||
state.inArray++;
|
||||
stream.next();
|
||||
return 'bracket';
|
||||
} else if (!state.lhs && stream.match(/^\-?\d+(?:\.\d+)?/)) {
|
||||
return 'number';
|
||||
} else if (!stream.eatSpace()) {
|
||||
stream.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME('text/x-toml', 'toml');
|
||||
|
||||
});
|
22
ui/html/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>RustDesk Server</title>
|
||||
<link rel="icon" href="data:;base64,=">
|
||||
<script>addEventListener('contextmenu', e => e.preventDefault());</script>
|
||||
<link rel="stylesheet" href="editor/codemirror.css">
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="editor/codemirror.js"></script>
|
||||
<script src="editor/toml.js"></script>
|
||||
<script type="module" src="/main.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<textarea></textarea>
|
||||
<form>
|
||||
<label><input type="checkbox"> <p>Turn on auto scroll</p></label>
|
||||
<label><p>Press ctrl + s to save</p></label>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
153
ui/html/main.js
Normal file
@ -0,0 +1,153 @@
|
||||
const { event, fs, path, tauri } = window.__TAURI__;
|
||||
|
||||
class View {
|
||||
constructor() {
|
||||
Object.assign(this, {
|
||||
content: '',
|
||||
action_time: 0,
|
||||
is_auto_scroll: true,
|
||||
is_edit_mode: false,
|
||||
is_file_changed: false,
|
||||
is_form_changed: false,
|
||||
is_content_changed: false
|
||||
}, ...arguments);
|
||||
addEventListener('DOMContentLoaded', this.init.bind(this));
|
||||
}
|
||||
async init() {
|
||||
this.editor = this.renderEditor();
|
||||
this.editor.on('scroll', this.editorScroll.bind(this));
|
||||
this.editor.on('keypress', this.editorSave.bind(this));
|
||||
this.form = this.renderForm();
|
||||
this.form.addEventListener('change', this.formChange.bind(this));
|
||||
event.listen('__update__', this.appAction.bind(this));
|
||||
event.emit('__action__', '__init__');
|
||||
while (true) {
|
||||
try {
|
||||
await this.update();
|
||||
this.render();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
async update() {
|
||||
if (this.is_file_changed) {
|
||||
this.is_file_changed = false;
|
||||
let now = Date.now(),
|
||||
file = await path.resolveResource(this.file);
|
||||
if (await fs.exists(file)) {
|
||||
let content = await fs.readTextFile(file);
|
||||
if (this.action_time < now) {
|
||||
this.content = content;
|
||||
this.is_content_changed = true;
|
||||
}
|
||||
} else {
|
||||
if (now >= this.action_time) {
|
||||
if (this.is_edit_mode) {
|
||||
this.content = `# https://github.com/rustdesk/rustdesk-server#env-variables
|
||||
RUST_LOG=info
|
||||
`;
|
||||
}
|
||||
this.is_content_changed = true;
|
||||
}
|
||||
console.warn(`${this.file} file is missing`);
|
||||
}
|
||||
}
|
||||
}
|
||||
async editorSave(editor, e) {
|
||||
if (e.ctrlKey && e.keyCode === 19 && this.is_edit_mode && !this.locked) {
|
||||
this.locked = true;
|
||||
try {
|
||||
let now = Date.now(),
|
||||
content = this.editor.doc.getValue(),
|
||||
file = await path.resolveResource(this.file);
|
||||
await fs.writeTextFile(file, content);
|
||||
event.emit('__action__', 'restart');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.locked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
editorScroll(e) {
|
||||
let info = this.editor.getScrollInfo(),
|
||||
distance = info.height - info.top - info.clientHeight,
|
||||
is_end = distance < 1;
|
||||
if (this.is_auto_scroll !== is_end) {
|
||||
this.is_auto_scroll = is_end;
|
||||
this.is_form_changed = true;
|
||||
}
|
||||
}
|
||||
formChange(e) {
|
||||
switch (e.target.tagName.toLowerCase()) {
|
||||
case 'input':
|
||||
this.is_auto_scroll = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
appAction(e) {
|
||||
let [action, data] = e.payload;
|
||||
switch (action) {
|
||||
case 'file':
|
||||
if (data === '.env') {
|
||||
this.is_edit_mode = true;
|
||||
this.file = `bin/${data}`;
|
||||
} else {
|
||||
this.is_edit_mode = false;
|
||||
this.file = `logs/${data}`;
|
||||
}
|
||||
this.action_time = Date.now();
|
||||
this.is_file_changed = true;
|
||||
this.is_form_changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.is_form_changed) {
|
||||
this.is_form_changed = false;
|
||||
this.renderForm();
|
||||
}
|
||||
if (this.is_content_changed) {
|
||||
this.is_content_changed = false;
|
||||
this.renderEditor();
|
||||
}
|
||||
if (this.is_auto_scroll && !this.is_edit_mode) {
|
||||
this.renderScrollbar();
|
||||
}
|
||||
}
|
||||
renderForm() {
|
||||
let form = this.form || document.querySelector('form'),
|
||||
label = form.querySelectorAll('label'),
|
||||
input = form.querySelector('input');
|
||||
input.checked = this.is_auto_scroll;
|
||||
if (this.is_edit_mode) {
|
||||
label[0].style.display = 'none';
|
||||
label[1].style.display = 'inline';
|
||||
} else {
|
||||
label[0].style.display = 'inline';
|
||||
label[1].style.display = 'none';
|
||||
}
|
||||
return form;
|
||||
}
|
||||
renderEditor() {
|
||||
let editor = this.editor || CodeMirror.fromTextArea(document.querySelector('textarea'), {
|
||||
mode: { name: 'toml' },
|
||||
lineNumbers: true,
|
||||
autofocus: true
|
||||
});
|
||||
editor.setOption('readOnly', !this.is_edit_mode);
|
||||
editor.doc.setValue(this.content);
|
||||
editor.doc.clearHistory();
|
||||
this.content = '';
|
||||
editor.focus();
|
||||
return editor;
|
||||
}
|
||||
renderScrollbar() {
|
||||
let info = this.editor.getScrollInfo();
|
||||
this.editor.scrollTo(info.left, info.height);
|
||||
}
|
||||
}
|
||||
|
||||
new View();
|
34
ui/html/style.css
Normal file
@ -0,0 +1,34 @@
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
height: calc(100vh - 20px);
|
||||
}
|
||||
|
||||
form {
|
||||
height: 20px;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 5px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
form>label {
|
||||
display: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
form>label>input,
|
||||
form>label>p {
|
||||
height: 19px;
|
||||
padding: 0;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
BIN
ui/icons/128x128.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
ui/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
ui/icons/32x32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
ui/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
ui/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
ui/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
ui/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
ui/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
ui/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
ui/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
ui/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
ui/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
ui/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
ui/icons/icon.icns
Normal file
BIN
ui/icons/icon.ico
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
ui/icons/icon.png
Normal file
After Width: | Height: | Size: 41 KiB |
108
ui/setup.nsi
Normal file
@ -0,0 +1,108 @@
|
||||
Unicode true
|
||||
|
||||
####################################################################
|
||||
# Includes
|
||||
|
||||
!include nsDialogs.nsh
|
||||
!include MUI2.nsh
|
||||
!include x64.nsh
|
||||
!include LogicLib.nsh
|
||||
|
||||
####################################################################
|
||||
# File Info
|
||||
|
||||
!define APP_NAME "RustDeskServer"
|
||||
!define PRODUCT_NAME "rustdesk_server"
|
||||
!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}"
|
||||
!define COPYRIGHT "Copyright © 2021"
|
||||
!define VERSION "1.1.7"
|
||||
|
||||
VIProductVersion "${VERSION}.0"
|
||||
VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
|
||||
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}"
|
||||
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
|
||||
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||
|
||||
####################################################################
|
||||
# Installer Attributes
|
||||
|
||||
Name "${APP_NAME}"
|
||||
Outfile "${APP_NAME}.Setup.exe"
|
||||
Caption "Setup - ${APP_NAME}"
|
||||
BrandingText "${APP_NAME}"
|
||||
|
||||
ShowInstDetails show
|
||||
RequestExecutionLevel admin
|
||||
SetOverwrite on
|
||||
|
||||
InstallDir "$PROGRAMFILES64\${APP_NAME}"
|
||||
|
||||
####################################################################
|
||||
# Pages
|
||||
|
||||
!define MUI_ICON "icons\icon.ico"
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_LANGDLL_ALLLANGUAGES
|
||||
!define MUI_FINISHPAGE_SHOWREADME ""
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Startup Shortcut"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateStartupShortcut
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe"
|
||||
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
####################################################################
|
||||
# Language
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
|
||||
####################################################################
|
||||
# Sections
|
||||
|
||||
Section "Install"
|
||||
SetShellVarContext all
|
||||
nsExec::Exec 'sc stop hbbr'
|
||||
nsExec::Exec 'sc stop hbbs'
|
||||
nsExec::Exec 'taskkill /F /IM ${PRODUCT_NAME}.exe'
|
||||
Sleep 500 ;
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
File /r "setup\*.*"
|
||||
WriteUninstaller $INSTDIR\uninstall.exe
|
||||
|
||||
CreateDirectory "$SMPROGRAMS\${APP_NAME}"
|
||||
CreateShortCut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
|
||||
CreateShortCut "$SMPROGRAMS\${APP_NAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
||||
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
|
||||
CreateShortCut "$SMSTARTUP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
|
||||
|
||||
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=in action=allow program="$INSTDIR\hbbs.exe" enable=yes'
|
||||
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\hbbs.exe" enable=yes'
|
||||
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=in action=allow program="$INSTDIR\hbbr.exe" enable=yes'
|
||||
nsExec::Exec 'netsh advfirewall firewall add rule name="${APP_NAME}" dir=out action=allow program="$INSTDIR\hbbr.exe" enable=yes'
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
SetShellVarContext all
|
||||
nsExec::Exec 'sc stop hbbr'
|
||||
nsExec::Exec 'sc stop hbbs'
|
||||
nsExec::Exec 'taskkill /F /IM ${PRODUCT_NAME}.exe'
|
||||
Sleep 500 ;
|
||||
|
||||
RMDir /r "$SMPROGRAMS\${APP_NAME}"
|
||||
Delete "$SMSTARTUP\${APP_NAME}.lnk"
|
||||
Delete "$DESKTOP\${APP_NAME}.lnk"
|
||||
nsExec::Exec 'sc delete hbbr'
|
||||
nsExec::Exec 'sc delete hbbs'
|
||||
nsExec::Exec 'netsh advfirewall firewall delete rule name="${APP_NAME}"'
|
||||
SectionEnd
|
||||
|
||||
####################################################################
|
||||
# Functions
|
||||
|
||||
Function CreateStartupShortcut
|
||||
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
|
||||
FunctionEnd
|
BIN
ui/setup/service/nssm.exe
Normal file
23
ui/setup/service/run.cmd
Normal file
@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
%~d0
|
||||
cd "%~dp0"
|
||||
set nssm="%cd%\nssm"
|
||||
cd ..
|
||||
|
||||
%nssm% install %1 "%cd%\bin\%1.exe"
|
||||
|
||||
%nssm% set %1 DisplayName %1
|
||||
%nssm% set %1 Description rustdesk %1 server
|
||||
%nssm% set %1 Start SERVICE_AUTO_START
|
||||
|
||||
%nssm% set %1 ObjectName LocalSystem
|
||||
%nssm% set %1 Type SERVICE_WIN32_OWN_PROCESS
|
||||
|
||||
%nssm% set %1 AppThrottle 1000
|
||||
%nssm% set %1 AppExit Default Restart
|
||||
%nssm% set %1 AppRestartDelay 0
|
||||
|
||||
%nssm% set %1 AppStdout "%cd%\logs\%1.out"
|
||||
%nssm% set %1 AppStderr "%cd%\logs\%1.err"
|
||||
|
||||
%nssm% start %1
|
5
ui/src/adapter/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod view;
|
||||
pub mod service;
|
||||
|
||||
pub use view::*;
|
||||
pub use service::*;
|
3
ui/src/adapter/service/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod windows;
|
||||
|
||||
pub use windows::*;
|
130
ui/src/adapter/service/windows.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use std::{ffi::OsStr, process::Command};
|
||||
|
||||
use crate::{path, usecase::service::*};
|
||||
use derive_new::new;
|
||||
use windows_service::{
|
||||
service::ServiceAccess,
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
#[derive(Debug, new)]
|
||||
pub struct WindowsDesktopService {
|
||||
#[new(value = "DesktopServiceState::Stopped")]
|
||||
pub state: DesktopServiceState,
|
||||
}
|
||||
|
||||
impl IDesktopService for WindowsDesktopService {
|
||||
fn start(&mut self) {
|
||||
call(
|
||||
[
|
||||
"echo.",
|
||||
"%nssm% stop hbbr",
|
||||
"%nssm% remove hbbr confirm",
|
||||
"%nssm% stop hbbs",
|
||||
"%nssm% remove hbbs confirm",
|
||||
"mkdir logs",
|
||||
"echo.",
|
||||
"service\\run.cmd hbbs",
|
||||
"echo.",
|
||||
"service\\run.cmd hbbr",
|
||||
"echo.",
|
||||
"@ping 127.1 -n 3 >nul",
|
||||
]
|
||||
.join(" & "),
|
||||
);
|
||||
self.check();
|
||||
}
|
||||
fn stop(&mut self) {
|
||||
call(
|
||||
[
|
||||
"echo.",
|
||||
"%nssm% stop hbbr",
|
||||
"%nssm% remove hbbr confirm",
|
||||
"echo.",
|
||||
"%nssm% stop hbbs",
|
||||
"%nssm% remove hbbs confirm",
|
||||
"echo.",
|
||||
"@ping 127.1 -n 3 >nul",
|
||||
]
|
||||
.join(" & "),
|
||||
);
|
||||
self.check();
|
||||
}
|
||||
fn restart(&mut self) {
|
||||
nssm(["restart", "hbbs"].map(|x| x.to_owned()));
|
||||
nssm(["restart", "hbbr"].map(|x| x.to_owned()));
|
||||
self.check();
|
||||
}
|
||||
fn pause(&mut self) {
|
||||
call(
|
||||
[
|
||||
"echo.",
|
||||
"%nssm% stop hbbr",
|
||||
"echo.",
|
||||
"%nssm% stop hbbs",
|
||||
"echo.",
|
||||
"@ping 127.1 -n 3 >nul",
|
||||
]
|
||||
.join(" & "),
|
||||
);
|
||||
self.check();
|
||||
}
|
||||
fn check(&mut self) -> DesktopServiceState {
|
||||
self.state = match service_status("hbbs").as_str() {
|
||||
"Running" => DesktopServiceState::Started,
|
||||
// "Stopped" => DeskServerServiceState::Paused,
|
||||
_ => DesktopServiceState::Stopped,
|
||||
};
|
||||
self.state.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn call(cmd: String) {
|
||||
Command::new("cmd")
|
||||
.current_dir(&path())
|
||||
.env("nssm", "service\\nssm.exe")
|
||||
.arg("/c")
|
||||
.arg("start")
|
||||
.arg("cmd")
|
||||
.arg("/c")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.expect("cmd exec error!");
|
||||
}
|
||||
|
||||
fn exec<I, S>(program: S, args: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
match Command::new(program).args(args).output() {
|
||||
Ok(out) => String::from_utf8(out.stdout).unwrap_or("".to_owned()),
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn nssm<I>(args: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
exec(
|
||||
format!("{}\\service\\nssm.exe", path().to_str().unwrap_or_default()),
|
||||
args,
|
||||
)
|
||||
.replace("\0", "")
|
||||
.trim()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn service_status(name: &str) -> String {
|
||||
match ServiceManager::local_computer(None::<&OsStr>, ServiceManagerAccess::CONNECT) {
|
||||
Ok(manager) => match manager.open_service(name, ServiceAccess::QUERY_STATUS) {
|
||||
Ok(service) => match service.query_status() {
|
||||
Ok(status) => format!("{:?}", status.current_state),
|
||||
Err(e) => e.to_string(),
|
||||
},
|
||||
Err(e) => e.to_string(),
|
||||
},
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
}
|
220
ui/src/adapter/view/desktop.rs
Normal file
@ -0,0 +1,220 @@
|
||||
use std::{
|
||||
process::exit,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
path,
|
||||
usecase::{view::Event, DesktopServiceState},
|
||||
BUFFER,
|
||||
};
|
||||
use async_std::task::sleep;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use tauri::{
|
||||
CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu,
|
||||
SystemTrayMenuItem, WindowEvent,
|
||||
};
|
||||
|
||||
pub async fn run(sender: Sender<Event>, receiver: Receiver<Event>) {
|
||||
let setup_sender = sender.clone();
|
||||
let menu_sender = sender.clone();
|
||||
let tray_sender = sender.clone();
|
||||
let menu = Menu::new()
|
||||
.add_submenu(Submenu::new(
|
||||
"Service",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("restart", "Restart"))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("start", "Start"))
|
||||
.add_item(CustomMenuItem::new("stop", "Stop")),
|
||||
))
|
||||
.add_submenu(Submenu::new(
|
||||
"Logs",
|
||||
Menu::new()
|
||||
.add_item(CustomMenuItem::new("hbbs.out", "hbbs.out"))
|
||||
.add_item(CustomMenuItem::new("hbbs.err", "hbbs.err"))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("hbbr.out", "hbbr.out"))
|
||||
.add_item(CustomMenuItem::new("hbbr.err", "hbbr.err")),
|
||||
))
|
||||
.add_submenu(Submenu::new(
|
||||
"Configuration",
|
||||
Menu::new().add_item(CustomMenuItem::new(".env", ".env")),
|
||||
));
|
||||
let tray = SystemTray::new().with_menu(
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("restart", "Restart"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("start", "Start"))
|
||||
.add_item(CustomMenuItem::new("stop", "Stop"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("exit", "Exit GUI")),
|
||||
);
|
||||
let mut app = tauri::Builder::default()
|
||||
.on_window_event(|event| match event.event() {
|
||||
// WindowEvent::Resized(size) => {
|
||||
// if size.width == 0 && size.height == 0 {
|
||||
// event.window().hide().unwrap();
|
||||
// }
|
||||
// }
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
event.window().hide().unwrap();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.menu(menu)
|
||||
.on_menu_event(move |event| {
|
||||
// println!(
|
||||
// "send {}: {}",
|
||||
// std::time::SystemTime::now()
|
||||
// .duration_since(std::time::UNIX_EPOCH)
|
||||
// .unwrap_or_default()
|
||||
// .as_millis(),
|
||||
// event.menu_item_id()
|
||||
// );
|
||||
menu_sender
|
||||
.send(Event::ViewAction(event.menu_item_id().to_owned()))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.system_tray(tray)
|
||||
.on_system_tray_event(move |app, event| match event {
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
let main = app.get_window("main").unwrap();
|
||||
if main.is_visible().unwrap() {
|
||||
main.hide().unwrap();
|
||||
} else {
|
||||
main.show().unwrap();
|
||||
main.unminimize().unwrap();
|
||||
main.set_focus().unwrap();
|
||||
}
|
||||
}
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => {
|
||||
tray_sender.send(Event::ViewAction(id)).unwrap_or_default();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.setup(move |app| {
|
||||
setup_sender.send(Event::ViewInit).unwrap_or_default();
|
||||
app.listen_global("__action__", move |msg| {
|
||||
match msg.payload().unwrap_or_default() {
|
||||
r#""__init__""# => setup_sender.send(Event::BroswerInit).unwrap_or_default(),
|
||||
r#""restart""# => setup_sender
|
||||
.send(Event::BrowserAction("restart".to_owned()))
|
||||
.unwrap_or_default(),
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![root])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
let mut now = Instant::now();
|
||||
let mut blink = false;
|
||||
let mut span = 0;
|
||||
let mut title = "".to_owned();
|
||||
let product = "RustDesk Server";
|
||||
let buffer = BUFFER.get().unwrap().to_owned();
|
||||
loop {
|
||||
for _ in 1..buffer {
|
||||
match receiver.recv_timeout(Duration::from_nanos(1)) {
|
||||
Ok(event) => {
|
||||
let main = app.get_window("main").unwrap();
|
||||
let menu = main.menu_handle();
|
||||
let tray = app.tray_handle();
|
||||
match event {
|
||||
Event::BrowserUpdate((action, data)) => match action.as_str() {
|
||||
"file" => {
|
||||
let list = ["hbbs.out", "hbbs.err", "hbbr.out", "hbbr.err", ".env"];
|
||||
let id = data.as_str();
|
||||
if list.contains(&id) {
|
||||
for file in list {
|
||||
menu.get_item(file)
|
||||
.set_selected(file == id)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
// println!(
|
||||
// "emit {}: {}",
|
||||
// std::time::SystemTime::now()
|
||||
// .duration_since(std::time::UNIX_EPOCH)
|
||||
// .unwrap_or_default()
|
||||
// .as_millis(),
|
||||
// data
|
||||
// );
|
||||
app.emit_all("__update__", (action, data))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Event::ViewRenderAppExit => exit(0),
|
||||
Event::ViewRenderServiceState(state) => {
|
||||
let enabled = |id, enabled| {
|
||||
menu.get_item(id).set_enabled(enabled).unwrap_or_default();
|
||||
tray.get_item(id).set_enabled(enabled).unwrap_or_default();
|
||||
};
|
||||
title = format!("{} {:?}", product, state);
|
||||
main.set_title(title.as_str()).unwrap_or_default();
|
||||
match state {
|
||||
DesktopServiceState::Started => {
|
||||
enabled("start", false);
|
||||
enabled("stop", true);
|
||||
enabled("restart", true);
|
||||
blink = false;
|
||||
}
|
||||
DesktopServiceState::Stopped => {
|
||||
enabled("start", true);
|
||||
enabled("stop", false);
|
||||
enabled("restart", false);
|
||||
blink = true;
|
||||
}
|
||||
_ => {
|
||||
enabled("start", false);
|
||||
enabled("stop", false);
|
||||
enabled("restart", false);
|
||||
blink = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
let elapsed = now.elapsed().as_micros();
|
||||
if elapsed > 16666 {
|
||||
now = Instant::now();
|
||||
// println!("{}ms", elapsed as f64 * 0.001);
|
||||
let iteration = app.run_iteration();
|
||||
if iteration.window_count == 0 {
|
||||
break;
|
||||
}
|
||||
if blink {
|
||||
if span > 1000000 {
|
||||
span = 0;
|
||||
app.get_window("main")
|
||||
.unwrap()
|
||||
.set_title(title.as_str())
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
span += elapsed;
|
||||
if span > 500000 {
|
||||
app.get_window("main")
|
||||
.unwrap()
|
||||
.set_title(product)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sleep(Duration::from_micros(999)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn root() -> String {
|
||||
path().to_str().unwrap_or_default().to_owned()
|
||||
}
|
3
ui/src/adapter/view/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod desktop;
|
||||
|
||||
pub use desktop::*;
|
17
ui/src/lib.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use std::{env::current_exe, path::PathBuf};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
pub mod adapter;
|
||||
pub mod usecase;
|
||||
|
||||
pub static BUFFER: OnceCell<usize> = OnceCell::new();
|
||||
|
||||
pub fn path() -> PathBuf {
|
||||
current_exe()
|
||||
.unwrap_or_default()
|
||||
.as_path()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
25
ui/src/main.rs
Normal file
@ -0,0 +1,25 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use async_std::{
|
||||
prelude::FutureExt,
|
||||
task::{spawn, spawn_local},
|
||||
};
|
||||
use crossbeam_channel::bounded;
|
||||
use rustdesk_server::{
|
||||
usecase::{presenter, view, watcher},
|
||||
BUFFER,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
let buffer = BUFFER.get_or_init(|| 10).to_owned();
|
||||
let (view_sender, presenter_receiver) = bounded(buffer);
|
||||
let (presenter_sender, view_receiver) = bounded(buffer);
|
||||
spawn_local(view::create(presenter_sender.clone(), presenter_receiver))
|
||||
.join(spawn(presenter::create(view_sender, view_receiver)))
|
||||
.join(spawn(watcher::create(presenter_sender)))
|
||||
.await;
|
||||
}
|
9
ui/src/usecase/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
pub mod presenter;
|
||||
pub mod service;
|
||||
pub mod view;
|
||||
pub mod watcher;
|
||||
|
||||
pub use presenter::*;
|
||||
pub use service::*;
|
||||
pub use view::*;
|
||||
pub use watcher::*;
|
59
ui/src/usecase/presenter.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::{service, DesktopServiceState, Event};
|
||||
use crate::BUFFER;
|
||||
use async_std::task::sleep;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
|
||||
let mut now = Instant::now();
|
||||
let buffer = BUFFER.get().unwrap().to_owned();
|
||||
let send = |event| sender.send(event).unwrap_or_default();
|
||||
if let Some(mut service) = service::create() {
|
||||
let mut service_state = DesktopServiceState::Unknown;
|
||||
let mut file = "hbbs.out".to_owned();
|
||||
send(Event::ViewRenderServiceState(service_state.to_owned()));
|
||||
loop {
|
||||
for _ in 1..buffer {
|
||||
match receiver.recv_timeout(Duration::from_nanos(1)) {
|
||||
Ok(event) => match event {
|
||||
Event::BroswerInit => {
|
||||
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
|
||||
}
|
||||
Event::BrowserAction(action) => match action.as_str() {
|
||||
"restart" => service.restart(),
|
||||
_ => (),
|
||||
},
|
||||
Event::FileChange(path) => {
|
||||
if path == file {
|
||||
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
|
||||
}
|
||||
}
|
||||
Event::ViewAction(action) => match action.as_str() {
|
||||
"start" => service.start(),
|
||||
"stop" => service.stop(),
|
||||
"restart" => service.restart(),
|
||||
"pause" => service.pause(),
|
||||
"exit" => send(Event::ViewRenderAppExit),
|
||||
_ => {
|
||||
file = action;
|
||||
send(Event::BrowserUpdate(("file".to_owned(), file.to_owned())));
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
},
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_micros(999)).await;
|
||||
if now.elapsed().as_millis() > 999 {
|
||||
let state = service.check();
|
||||
if state != service_state {
|
||||
service_state = state.to_owned();
|
||||
send(Event::ViewRenderServiceState(state));
|
||||
}
|
||||
now = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
ui/src/usecase/service.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use crate::adapter;
|
||||
|
||||
pub fn create() -> Option<Box<dyn IDesktopService + Send>> {
|
||||
if cfg!(target_os = "windows") {
|
||||
return Some(Box::new(adapter::WindowsDesktopService::new()));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DesktopServiceState {
|
||||
Paused,
|
||||
Started,
|
||||
Stopped,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub trait IDesktopService {
|
||||
fn start(&mut self);
|
||||
fn stop(&mut self);
|
||||
fn restart(&mut self);
|
||||
fn pause(&mut self);
|
||||
fn check(&mut self) -> DesktopServiceState;
|
||||
}
|
22
ui/src/usecase/view.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use super::DesktopServiceState;
|
||||
use crate::adapter::desktop;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
|
||||
pub async fn create(sender: Sender<Event>, receiver: Receiver<Event>) {
|
||||
desktop::run(sender, receiver).await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Event {
|
||||
BrowserAction(String),
|
||||
BroswerInit,
|
||||
BrowserUpdate((String, String)),
|
||||
BrowserRender(String),
|
||||
FileChange(String),
|
||||
ViewAction(String),
|
||||
ViewInit,
|
||||
ViewUpdate(String),
|
||||
ViewRender(String),
|
||||
ViewRenderAppExit,
|
||||
ViewRenderServiceState(DesktopServiceState),
|
||||
}
|
46
ui/src/usecase/watcher.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use super::Event;
|
||||
use crate::path;
|
||||
use async_std::task::{sleep, spawn_blocking};
|
||||
use crossbeam_channel::{bounded, Sender};
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Result, Watcher};
|
||||
|
||||
pub async fn create(sender: Sender<Event>) {
|
||||
loop {
|
||||
let watch_sender = sender.clone();
|
||||
match spawn_blocking(|| {
|
||||
watch(
|
||||
format!("{}/logs/", path().to_str().unwrap_or_default()),
|
||||
watch_sender,
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(e) => println!("error: {e}"),
|
||||
}
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn watch<P: AsRef<Path>>(path: P, sender: Sender<Event>) -> Result<()> {
|
||||
let (tx, rx) = bounded(10);
|
||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
||||
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
|
||||
for res in rx {
|
||||
let event = res?;
|
||||
for p in event.paths {
|
||||
let path = p
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
if path.len() > 0 {
|
||||
sender.send(Event::FileChange(path)).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
89
ui/tauri.conf.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"devPath": "html",
|
||||
"distDir": "html",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
"productName": "rustdesk_server",
|
||||
"version": "0.1.1"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"fs": {
|
||||
"scope": [
|
||||
"$RESOURCE/bin/.env",
|
||||
"$RESOURCE/logs/*"
|
||||
],
|
||||
"all": false,
|
||||
"exists": true,
|
||||
"readDir": true,
|
||||
"readFile": true,
|
||||
"writeFile": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "rustdesk_server",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"systemTray": {
|
||||
"iconPath": "icons/icon.ico",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"center": true,
|
||||
"fullscreen": false,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"title": "RustDesk Server",
|
||||
"width": 980
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|