mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
wip: localization
This commit is contained in:
195
core/Cargo.lock
generated
195
core/Cargo.lock
generated
@@ -239,6 +239,15 @@ dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "archery"
|
||||
version = "1.2.2"
|
||||
@@ -848,6 +857,12 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076"
|
||||
|
||||
[[package]]
|
||||
name = "base62"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adf9755786e27479693dedd3271691a92b5e242ab139cacb9fb8e7fb5381111"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
@@ -1109,7 +1124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-automata 0.4.13",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -3094,12 +3109,42 @@ version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "glob-match"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.13",
|
||||
"regex-syntax 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
@@ -3740,6 +3785,22 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.13",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.9"
|
||||
@@ -4254,7 +4315,7 @@ version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
"regex-automata 0.4.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4479,11 +4540,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4784,6 +4845,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.2.0"
|
||||
@@ -4818,11 +4888,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5199,6 +5270,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.3"
|
||||
@@ -6364,10 +6441,19 @@ checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-automata 0.4.13",
|
||||
"regex-syntax 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
@@ -6585,6 +6671,60 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332"
|
||||
dependencies = [
|
||||
"globwalk",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"rust-i18n-macro",
|
||||
"rust-i18n-support",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-macro"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-i18n-support",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-support"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"base62",
|
||||
"globwalk",
|
||||
"itertools 0.11.0",
|
||||
"lazy_static",
|
||||
"normpath",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"siphasher",
|
||||
"toml 0.8.23",
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
@@ -7106,6 +7246,19 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yml"
|
||||
version = "0.0.12"
|
||||
@@ -7763,6 +7916,7 @@ dependencies = [
|
||||
"rpassword",
|
||||
"rpc-toolkit",
|
||||
"rust-argon2",
|
||||
"rust-i18n",
|
||||
"safelog",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -9616,14 +9770,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
@@ -9653,6 +9807,17 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@@ -9854,6 +10019,12 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -213,6 +213,7 @@ reqwest = { version = "0.12.25", features = [
|
||||
reqwest_cookie_store = "0.9.0"
|
||||
rpassword = "7.2.0"
|
||||
rust-argon2 = "3.0.0"
|
||||
rust-i18n = "3.1.5"
|
||||
rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git" }
|
||||
safelog = { version = "0.4.8", git = "https://github.com/Start9Labs/arti.git", branch = "patch/disable-exit", optional = true }
|
||||
semver = { version = "1.0.20", features = ["serde"] }
|
||||
@@ -263,7 +264,7 @@ tower-service = "0.3.3"
|
||||
tracing = "0.1.39"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-journald = "0.3.0"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-subscriber = { version = "=0.3.19", features = ["env-filter"] }
|
||||
ts-rs = "9.0.1"
|
||||
typed-builder = "0.23.2"
|
||||
url = { version = "2.4.1", features = ["serde"] }
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::net::utils::ipv6_is_local;
|
||||
use crate::net::vhost::AlpnInfo;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::FullProgress;
|
||||
use crate::system::SmtpValue;
|
||||
use crate::system::{KeyboardOptions, SmtpValue};
|
||||
use crate::util::cpupower::Governor;
|
||||
use crate::util::lshw::LshwDevice;
|
||||
use crate::util::serde::MaybeUtf8String;
|
||||
@@ -139,6 +139,8 @@ impl Public {
|
||||
ram: 0,
|
||||
devices: Vec::new(),
|
||||
kiosk,
|
||||
language: None,
|
||||
keyboard: None,
|
||||
},
|
||||
package_data: AllPackageData::default(),
|
||||
ui: serde_json::from_str(*DB_UI_SEED_CELL.get().unwrap_or(&"null"))
|
||||
@@ -195,6 +197,8 @@ pub struct ServerInfo {
|
||||
pub ram: u64,
|
||||
pub devices: Vec<LshwDevice>,
|
||||
pub kiosk: Option<bool>,
|
||||
pub language: Option<InternedString>,
|
||||
pub keyboard: Option<KeyboardOptions>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
|
||||
@@ -97,6 +97,7 @@ pub enum ErrorKind {
|
||||
InstallFailed = 76,
|
||||
UpdateFailed = 77,
|
||||
Smtp = 78,
|
||||
SetSysInfo = 79,
|
||||
}
|
||||
impl ErrorKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
@@ -180,6 +181,7 @@ impl ErrorKind {
|
||||
InstallFailed => "Install Failed",
|
||||
UpdateFailed => "Update Failed",
|
||||
Smtp => "SMTP Error",
|
||||
SetSysInfo => "Error Setting System Info",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
core/src/keyboard.conf.template
Normal file
8
core/src/keyboard.conf.template
Normal file
@@ -0,0 +1,8 @@
|
||||
Section "InputClass"
|
||||
Identifier "system-keyboard"
|
||||
MatchIsKeyboard "on"
|
||||
Option "XkbModel" "{model}"
|
||||
Option "XkbLayout" "{layout}"
|
||||
Option "XkbVariant" "{variant}"
|
||||
Option "XkbOptions" "{options}"
|
||||
EndSection
|
||||
@@ -1,5 +1,7 @@
|
||||
use const_format::formatcp;
|
||||
|
||||
rust_i18n::i18n!("locales", fallback = ["en_US"]);
|
||||
|
||||
pub const DATA_DIR: &str = "/media/startos/data";
|
||||
pub const MAIN_DATA: &str = formatcp!("{DATA_DIR}/main");
|
||||
pub const PACKAGE_DATA: &str = formatcp!("{DATA_DIR}/package-data");
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::cpupower::{Governor, get_available_governors, set_governor};
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::io::{copy_file, open_file, write_file_atomic};
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
use crate::util::sync::Watch;
|
||||
use crate::{MAIN_DATA, PACKAGE_DATA};
|
||||
@@ -1053,6 +1053,105 @@ pub async fn test_smtp(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KeyboardOptions {
|
||||
pub layout: InternedString,
|
||||
pub model: Option<InternedString>,
|
||||
pub variant: Option<InternedString>,
|
||||
#[arg(short, long = "option")]
|
||||
#[serde(default)]
|
||||
pub options: Vec<InternedString>,
|
||||
}
|
||||
impl KeyboardOptions {
|
||||
/// NOTE: will error if kiosk inactive
|
||||
pub async fn apply_to_session(&self) -> Result<(), Error> {
|
||||
let mut cmd = Command::new("setxkbmap");
|
||||
cmd.env("DISPLAY", ":0")
|
||||
.env("XAUTHORITY", "/home/kiosk/.Xauthority");
|
||||
cmd.arg("-layout").arg(&*self.layout);
|
||||
if let Some(variant) = self.variant.as_deref() {
|
||||
cmd.arg("-variant").arg(variant);
|
||||
}
|
||||
for option in &self.options {
|
||||
cmd.arg("-option").arg(&**option);
|
||||
}
|
||||
cmd.invoke(ErrorKind::SetSysInfo).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> Result<(), Error> {
|
||||
// TODO: set console keyboard
|
||||
write_file_atomic(
|
||||
"/media/startos/config/overlay/etc/X11/xorg.conf.d/00-keyboard.conf",
|
||||
format!(
|
||||
include_str!("./keyboard.conf.template"),
|
||||
model = self.model.as_deref().unwrap_or_default(),
|
||||
layout = &*self.layout,
|
||||
variant = self.variant.as_deref().unwrap_or_default(),
|
||||
options = self.options.join(","),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_keyboard(ctx: RpcContext, options: KeyboardOptions) -> Result<(), Error> {
|
||||
options.apply_to_session().await.log_err();
|
||||
options.save().await?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_keyboard_mut()
|
||||
.ser(&Some(options))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetLanguageParams {
|
||||
pub language: InternedString,
|
||||
}
|
||||
|
||||
pub async fn set_language(
|
||||
ctx: RpcContext,
|
||||
SetLanguageParams { language }: SetLanguageParams,
|
||||
) -> Result<(), Error> {
|
||||
write_file_atomic(
|
||||
"/etc/locale.gen",
|
||||
format!("{language}.UTF-8 UTF-8\n").as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
Command::new("locale-gen")
|
||||
.invoke(ErrorKind::SetSysInfo)
|
||||
.await?;
|
||||
copy_file(
|
||||
"/usr/lib/locale/locale-archive",
|
||||
"/media/startos/config/overlay/usr/lib/locale/locale-archive",
|
||||
)
|
||||
.await?;
|
||||
write_file_atomic(
|
||||
"/media/startos/config/overlay/etc/default/locale",
|
||||
format!("{language}.UTF-8\n").as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_public_mut()
|
||||
.as_server_info_mut()
|
||||
.as_language_mut()
|
||||
.ser(&Some(language.clone()))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
rust_i18n::set_locale(&*language);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
pub async fn test_get_temp() {
|
||||
|
||||
@@ -775,6 +775,23 @@ pub fn dir_copy<'a, P0: AsRef<Path> + 'a + Send + Sync, P1: AsRef<Path> + 'a + S
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub async fn copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<(), Error> {
|
||||
let src = src.as_ref();
|
||||
tokio::fs::metadata(src)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, src.display()))?;
|
||||
let dst = dst.as_ref();
|
||||
if let Some(parent) = dst.parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("mkdir -p {parent:?}")))?;
|
||||
}
|
||||
tokio::fs::copy(src, dst)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("cp {src:?} -> {dst:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub struct TimeoutStream<S: AsyncRead + AsyncWrite = TcpStream> {
|
||||
timeout: Duration,
|
||||
|
||||
Reference in New Issue
Block a user