Compare commits
6 commits
ac53850fd1
...
420fac5f18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
420fac5f18 | ||
|
|
aed73e99be | ||
|
|
88be53cd2f | ||
|
|
bd3ec3a904 | ||
|
|
e016c37634 | ||
|
|
373e300b59 |
11 changed files with 102 additions and 83 deletions
10
.cargo/config.toml
Normal file
10
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# SPDX-FileCopyrightText: 2026 Qyriad <qyriad@qyriad.me>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: EUPL-1.1
|
||||||
|
# vim: tabstop=2 shiftwidth=0 noexpandtab
|
||||||
|
|
||||||
|
[build]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "force-frame-pointers=yes",
|
||||||
|
"-C", "force-unwind-tables=yes",
|
||||||
|
]
|
||||||
16
Cargo.toml
16
Cargo.toml
|
|
@ -47,3 +47,19 @@ tap = "1.0.1"
|
||||||
tracing = { version = "0.1.44", features = ["attributes"] }
|
tracing = { version = "0.1.44", features = ["attributes"] }
|
||||||
tracing-human-layer = "0.2.1"
|
tracing-human-layer = "0.2.1"
|
||||||
tracing-subscriber = { version = "0.3.22", default-features = false, features = ["std", "env-filter", "fmt", "ansi", "registry", "parking_lot"] }
|
tracing-subscriber = { version = "0.3.22", default-features = false, features = ["std", "env-filter", "fmt", "ansi", "registry", "parking_lot"] }
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
debug-assertions = true
|
||||||
|
lto = true
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[profile.release.package."*"]
|
||||||
|
debug = true
|
||||||
|
debug-assertions = true
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@
|
||||||
in import src { inherit pkgs; },
|
in import src { inherit pkgs; },
|
||||||
}: let
|
}: let
|
||||||
inherit (qpkgs) lib;
|
inherit (qpkgs) lib;
|
||||||
dynix = qpkgs.callPackage ./package.nix { }
|
dynix = (qpkgs.callPackage ./package.nix { })
|
||||||
|
.overrideAttrs (final: prev: {
|
||||||
|
dynixCommand = qpkgs.stdlib.mkStdenvPretty prev.dynixCommand;
|
||||||
|
dynixModules = qpkgs.stdlib.mkStdenvPretty prev.dynixModules;
|
||||||
|
})
|
||||||
|> qpkgs.stdlib.mkStdenvPretty;
|
|> qpkgs.stdlib.mkStdenvPretty;
|
||||||
byStdenv = lib.mapAttrs (stdenvName: stdenv: let
|
byStdenv = lib.mapAttrs (stdenvName: stdenv: let
|
||||||
withStdenv = dynix.override { clangStdenv = stdenv; };
|
withStdenv = dynix.override { clangStdenv = stdenv; };
|
||||||
|
|
|
||||||
12
flake.lock
generated
12
flake.lock
generated
|
|
@ -3,11 +3,11 @@
|
||||||
"fenix": {
|
"fenix": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1773126504,
|
"lastModified": 1774163246,
|
||||||
"narHash": "sha256-/iXlg2V5UMlgCmyRHkPHjlD6NdMfFOnwFMvH7REigD4=",
|
"narHash": "sha256-gzlqyLjP44LWraUd3Zn4xrQKOtK+zcBJ77pnsSUsxcM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "64407ddb1932af06ed5cd711f6a2ed946b2548b9",
|
"rev": "4cd28929c68cae521589bc21958d3793904ed1e2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -37,11 +37,11 @@
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1772956932,
|
"lastModified": 1773840656,
|
||||||
"narHash": "sha256-M0yS4AafhKxPPmOHGqIV0iKxgNO8bHDWdl1kOwGBwRY=",
|
"narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "608d0cadfed240589a7eea422407a547ad626a14",
|
"rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
11
package.nix
11
package.nix
|
|
@ -7,14 +7,23 @@
|
||||||
clangStdenv,
|
clangStdenv,
|
||||||
callPackage,
|
callPackage,
|
||||||
linkFarm,
|
linkFarm,
|
||||||
|
llvmPackages,
|
||||||
rustHooks,
|
rustHooks,
|
||||||
rustPackages,
|
rustPackages,
|
||||||
versionCheckHook,
|
versionCheckHook,
|
||||||
|
wrapBintoolsWith,
|
||||||
}: lib.callWith' rustPackages ({
|
}: lib.callWith' rustPackages ({
|
||||||
rustPlatform,
|
rustPlatform,
|
||||||
cargo,
|
cargo,
|
||||||
}: let
|
}: let
|
||||||
stdenv = clangStdenv;
|
# Use LLD for faster link times.
|
||||||
|
stdenv = clangStdenv.override {
|
||||||
|
cc = clangStdenv.cc.override {
|
||||||
|
bintools = wrapBintoolsWith {
|
||||||
|
bintools = llvmPackages.bintools;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
cargoToml = lib.importTOML ./Cargo.toml;
|
cargoToml = lib.importTOML ./Cargo.toml;
|
||||||
cargoPackage = cargoToml.package;
|
cargoPackage = cargoToml.package;
|
||||||
in stdenv.mkDerivation (finalAttrs: let
|
in stdenv.mkDerivation (finalAttrs: let
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
inherit (pkgs) lib;
|
inherit (pkgs) lib;
|
||||||
|
|
||||||
mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { inherit fenixToolchain; };
|
mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { inherit fenixToolchain; };
|
||||||
devShell = mkDevShell dynix;
|
devShell = mkDevShell dynix |> qpkgs.stdlib.mkStdenvPretty;
|
||||||
|
|
||||||
byStdenv = lib.mapAttrs (lib.const mkDevShell) dynix.byStdenv;
|
byStdenv = lib.mapAttrs (lib.const mkDevShell) dynix.byStdenv;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
env, io, mem,
|
env, io,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
os::fd::{AsFd, BorrowedFd, IntoRawFd, OwnedFd, RawFd},
|
os::fd::{AsFd, BorrowedFd, IntoRawFd, OwnedFd, RawFd},
|
||||||
sync::{
|
sync::{
|
||||||
|
|
@ -26,7 +26,7 @@ use crate::{
|
||||||
|
|
||||||
use crate::{OwnedFdWithFlags, TokenFd};
|
use crate::{OwnedFdWithFlags, TokenFd};
|
||||||
|
|
||||||
pub static UID: LazyLock<Uid> = LazyLock::new(|| rustix::process::getuid());
|
pub static UID: LazyLock<Uid> = LazyLock::new(rustix::process::getuid);
|
||||||
|
|
||||||
pub static USER_SOCKET_DIR: LazyLock<&'static Path> = LazyLock::new(|| {
|
pub static USER_SOCKET_DIR: LazyLock<&'static Path> = LazyLock::new(|| {
|
||||||
let dir: Box<Path> = env::var_os("XDG_RUNTIME_DIR")
|
let dir: Box<Path> = env::var_os("XDG_RUNTIME_DIR")
|
||||||
|
|
@ -52,19 +52,23 @@ pub enum ConvenientAttrPath {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConvenientAttrPath {
|
impl ConvenientAttrPath {
|
||||||
|
/// Not currently used, but here for completeness.
|
||||||
|
#[expect(dead_code)]
|
||||||
pub fn clone_from_dotted(s: &str) -> Self {
|
pub fn clone_from_dotted(s: &str) -> Self {
|
||||||
Self::Dotted(Box::from(s))
|
Self::Dotted(Box::from(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Not currently used, but here for completeness.
|
||||||
|
#[expect(dead_code)]
|
||||||
pub fn clone_from_split(s: &[&str]) -> Self {
|
pub fn clone_from_split(s: &[&str]) -> Self {
|
||||||
Self::from_str_iter(s.into_iter().map(Deref::deref))
|
Self::from_str_iter(s.iter().map(Deref::deref))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str_iter<'i, I>(iter: I) -> Self
|
pub fn from_str_iter<'i, I>(iter: I) -> Self
|
||||||
where
|
where
|
||||||
I: Iterator<Item = &'i str>,
|
I: Iterator<Item = &'i str>,
|
||||||
{
|
{
|
||||||
let boxed = iter.map(|s| Box::from(s));
|
let boxed = iter.map(Box::from);
|
||||||
Self::Split(Box::from_iter(boxed))
|
Self::Split(Box::from_iter(boxed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,9 +139,6 @@ pub struct Daemon {
|
||||||
|
|
||||||
// Bijective mapping of [`mio::Token`]s to [`RawFd`]s.
|
// Bijective mapping of [`mio::Token`]s to [`RawFd`]s.
|
||||||
tokfd: BiHashMap<TokenFd>,
|
tokfd: BiHashMap<TokenFd>,
|
||||||
|
|
||||||
cmd_buffer: Vec<u8>,
|
|
||||||
next_timeout: Option<Duration>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `tokfd` handling.
|
/// `tokfd` handling.
|
||||||
|
|
@ -222,6 +223,8 @@ impl Daemon {
|
||||||
.copied()
|
.copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Not currently used, but here for completeness.
|
||||||
|
#[expect(dead_code)]
|
||||||
fn token_for_fd(&self, fd: RawFd) -> Option<Token> {
|
fn token_for_fd(&self, fd: RawFd) -> Option<Token> {
|
||||||
self.tokfd
|
self.tokfd
|
||||||
.get2(&fd)
|
.get2(&fd)
|
||||||
|
|
@ -255,10 +258,10 @@ impl Daemon {
|
||||||
|
|
||||||
debug!("opened daemon to {:?} file descriptor {fd:?}", name);
|
debug!("opened daemon to {:?} file descriptor {fd:?}", name);
|
||||||
|
|
||||||
let path = match &name {
|
let path = name
|
||||||
Some(name) => Some(PathBuf::from(name).into_boxed_path()),
|
.as_ref()
|
||||||
None => None,
|
.map(PathBuf::from)
|
||||||
};
|
.map(PathBuf::into_boxed_path);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
config_path,
|
config_path,
|
||||||
|
|
@ -267,8 +270,6 @@ impl Daemon {
|
||||||
poller,
|
poller,
|
||||||
fd_info,
|
fd_info,
|
||||||
tokfd: Default::default(),
|
tokfd: Default::default(),
|
||||||
cmd_buffer: Vec::with_capacity(1024),
|
|
||||||
next_timeout: TIMEOUT_NEVER,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,16 +346,12 @@ impl Daemon {
|
||||||
/// Private helpers.
|
/// Private helpers.
|
||||||
impl Daemon {
|
impl Daemon {
|
||||||
fn read_cmd(&mut self, fd: &BorrowedFd) -> Result<(), IoError> {
|
fn read_cmd(&mut self, fd: &BorrowedFd) -> Result<(), IoError> {
|
||||||
if self.cmd_buffer.len() == self.cmd_buffer.capacity() {
|
// FIXME: don't use a new allocation every time.
|
||||||
self.cmd_buffer.reserve(1024);
|
let mut cmd_buffer: Vec<u8> = Vec::with_capacity(1024);
|
||||||
}
|
|
||||||
|
|
||||||
let _count = rustix::io::read(fd, spare_capacity(&mut self.cmd_buffer))
|
let _count = rustix::io::read(fd, spare_capacity(&mut cmd_buffer))
|
||||||
.tap_err(|e| error!("read() on daemon fd {fd:?} failed: {e}"))?;
|
.tap_err(|e| error!("read() on daemon fd {fd:?} failed: {e}"))?;
|
||||||
|
|
||||||
// So that the loop doesn't borrow from `self`.
|
|
||||||
let mut cmd_buffer = mem::take(&mut self.cmd_buffer);
|
|
||||||
|
|
||||||
// The buffer might have existing data from the last read.
|
// The buffer might have existing data from the last read.
|
||||||
let deserializer = serde_json::Deserializer::from_slice(&cmd_buffer);
|
let deserializer = serde_json::Deserializer::from_slice(&cmd_buffer);
|
||||||
let stream: StreamDeserializer<_, DaemonCmd> = deserializer.into_iter();
|
let stream: StreamDeserializer<_, DaemonCmd> = deserializer.into_iter();
|
||||||
|
|
@ -362,16 +359,13 @@ impl Daemon {
|
||||||
let cmd = match cmd {
|
let cmd = match cmd {
|
||||||
Ok(cmd) => cmd,
|
Ok(cmd) => cmd,
|
||||||
Err(e) if e.is_eof() => {
|
Err(e) if e.is_eof() => {
|
||||||
self.next_timeout = Some(Duration::from_secs(4));
|
warn!("Got EOF before a valid command");
|
||||||
warn!("Didn't get a valid daemon command; giving the other side 4 seconds...");
|
debug!("command buffer was: {:?}", cmd_buffer.as_bstr());
|
||||||
let _ = mem::replace(&mut self.cmd_buffer, cmd_buffer);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("error deserializing command: {e}");
|
warn!("error deserializing command: {e}");
|
||||||
debug!("command buffer was: {:?}", cmd_buffer.as_bstr());
|
debug!("command buffer was: {:?}", cmd_buffer.as_bstr());
|
||||||
cmd_buffer.clear();
|
|
||||||
let _ = mem::replace(&mut self.cmd_buffer, cmd_buffer);
|
|
||||||
// Don't propagate the error unless we have too many.
|
// Don't propagate the error unless we have too many.
|
||||||
self.fd_error_push(fd.as_raw_fd(), e.into()).tap_err(|e| {
|
self.fd_error_push(fd.as_raw_fd(), e.into()).tap_err(|e| {
|
||||||
error!("Accumulated too many errors for daemon fd {fd:?}: {e}")
|
error!("Accumulated too many errors for daemon fd {fd:?}: {e}")
|
||||||
|
|
@ -385,9 +379,6 @@ impl Daemon {
|
||||||
self.dispatch_cmd(cmd).unwrap_or_else(|e| todo!("{e}"));
|
self.dispatch_cmd(cmd).unwrap_or_else(|e| todo!("{e}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_buffer.clear();
|
|
||||||
let _ = mem::replace(&mut self.cmd_buffer, cmd_buffer);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -469,25 +460,19 @@ impl Daemon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(timeout) = self.next_timeout {
|
match self.poller.poll(&mut events, TIMEOUT_NEVER) {
|
||||||
debug!(
|
|
||||||
"epoll_wait() with a timeout: {}",
|
|
||||||
humantime::format_duration(timeout),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.poller.poll(&mut events, self.next_timeout.take()) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
trace!(
|
trace!(
|
||||||
"mio::Poller::poll() got events: {:?}",
|
"mio::Poller::poll() got events: {:?}",
|
||||||
events.iter().size_hint().0,
|
events.iter().size_hint().0,
|
||||||
);
|
);
|
||||||
if events.is_empty() {
|
if events.is_empty() {
|
||||||
warn!("timeout expired");
|
unreachable!(
|
||||||
self.cmd_buffer.clear();
|
"epoll_wait() with a \"forever\" timeout should never give empty events",
|
||||||
} else {
|
);
|
||||||
let _ = self.fd_error_pop(self.poller.as_raw_fd());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = self.fd_error_pop(self.poller.as_raw_fd());
|
||||||
},
|
},
|
||||||
Err(e) if e.kind() == IoErrorKind::Interrupted => {
|
Err(e) if e.kind() == IoErrorKind::Interrupted => {
|
||||||
// EINTR is silly.
|
// EINTR is silly.
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ impl From<OwnedFd> for OwnedFdWithFlags {
|
||||||
|
|
||||||
impl Read for &OwnedFdWithFlags {
|
impl Read for &OwnedFdWithFlags {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize, IoError> {
|
fn read(&mut self, buf: &mut [u8]) -> Result<usize, IoError> {
|
||||||
debug_assert!(buf.len() > 0);
|
debug_assert!(!buf.is_empty());
|
||||||
loop {
|
loop {
|
||||||
buf.fill(0);
|
buf.fill(0);
|
||||||
match rustix::io::read(self.as_ref_owned(), &mut *buf) {
|
match rustix::io::read(self.as_ref_owned(), &mut *buf) {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ impl FdInfo {
|
||||||
|
|
||||||
match Self::guess_name(self.fd) {
|
match Self::guess_name(self.fd) {
|
||||||
Ok(name) => {
|
Ok(name) => {
|
||||||
let prev = self.name.set(Box::from(name));
|
let prev = self.name.set(name);
|
||||||
debug_assert_eq!(prev, Ok(()));
|
debug_assert_eq!(prev, Ok(()));
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -105,13 +105,14 @@ impl<'a> Display for FdInfoDisplay<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy)]
|
#[derive(Copy)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum FdKind {
|
pub enum FdKind {
|
||||||
File,
|
File,
|
||||||
Socket,
|
Socket,
|
||||||
SockStream,
|
SockStream,
|
||||||
Poller,
|
Poller,
|
||||||
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,12 +129,6 @@ impl FdKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FdKind {
|
|
||||||
fn default() -> FdKind {
|
|
||||||
FdKind::Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy)]
|
#[derive(Copy)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct TokenFd {
|
pub struct TokenFd {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt};
|
||||||
|
|
||||||
fn main_wrapped() -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
|
fn main_wrapped() -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
|
||||||
// Default RUST_LOG to warn if it's not specified.
|
// Default RUST_LOG to warn if it's not specified.
|
||||||
if let None = env::var_os("RUST_LOG") {
|
if env::var_os("RUST_LOG").is_none() {
|
||||||
unsafe {
|
unsafe {
|
||||||
env::set_var("RUST_LOG", "warn");
|
env::set_var("RUST_LOG", "warn");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,27 @@
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
//#[derive(Debug, Clone, PartialEq, Hash)]
|
||||||
pub(crate) struct NixEvalExpr<E, A> {
|
//pub(crate) struct NixEvalExpr<E, A> {
|
||||||
pub(crate) expr: E,
|
// pub(crate) expr: E,
|
||||||
pub(crate) attrpath: A,
|
// pub(crate) attrpath: A,
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
impl<E, A> NixEvalExpr<E, A>
|
//impl<E, A> NixEvalExpr<E, A>
|
||||||
where
|
//where
|
||||||
E: AsRef<OsStr>,
|
// E: AsRef<OsStr>,
|
||||||
A: AsRef<OsStr>,
|
// A: AsRef<OsStr>,
|
||||||
{
|
//{
|
||||||
pub(crate) fn into_command(self) -> Command {
|
// pub(crate) fn into_command(self) -> Command {
|
||||||
let mut cmd = Command::new("nix-instantiate");
|
// let mut cmd = Command::new("nix-instantiate");
|
||||||
cmd.arg("--eval")
|
// cmd.arg("--eval")
|
||||||
.arg("--json")
|
// .arg("--json")
|
||||||
.arg("--strict")
|
// .arg("--strict")
|
||||||
.arg("--expr")
|
// .arg("--expr")
|
||||||
.arg(self.expr)
|
// .arg(self.expr)
|
||||||
.arg("-A")
|
// .arg("-A")
|
||||||
.arg(self.attrpath);
|
// .arg(self.attrpath);
|
||||||
|
//
|
||||||
cmd
|
// cmd
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue