Created Boardersv2 moved old to Boardersv1

This commit is contained in:
Solomon W. 2024-05-17 14:46:41 -04:00
parent fa924285b0
commit 2571ce5756
37 changed files with 499 additions and 455 deletions

8
.gitignore vendored
View File

@ -1,8 +0,0 @@
/target
# new data directory, specified in config gile
# NOTE: this config file does not yet exist
/data
Cargo.lock

View File

@ -1,447 +0,0 @@
use std::{
env, fs, io,
io::{Write, Read},
sync::Mutex,
fs::ReadDir,
path::Path,
error::Error,
process::Command
};
use actix_web::{
get, web,
App, HttpResponse, HttpServer, Responder,
};
mod lib;
use lib::{
*,
Archiveable,
boards::{
Board, BoardFactory
},
users::UserManager,
messages::MessageFactory
};
mod api;
use api::types::AppState;
mod html;
use html::{
accounts::*,
boards::*
};
use tokio::{
sync::{oneshot, watch},
net::{
UnixStream, UnixListener,
},
runtime::Runtime,
select
};
use handlebars::{Handlebars, handlebars_helper};
use console::style;
use maplit::btreemap;
use simple_logger::{SimpleLogger};
use log::LevelFilter;
use serde::{Serialize, Deserialize};
pub struct ThreadData<'a> {
handlebars: Handlebars<'a>
}
#[get("/")]
async fn index(
tdata: web::Data<ThreadData<'_>>
) -> impl Responder {
HttpResponse::Ok().body(
tdata.handlebars.render("welcome", &btreemap!(
"version" => env!("CARGO_PKG_VERSION").to_string()
)).unwrap()
)
}
#[get("/debug/")]
async fn debug(
data: web::Data<AppState>
) -> impl Responder {
HttpResponse::Ok().body(serde_json::to_string_pretty(data.as_ref()).unwrap())
}
async fn socket_loop(socket: &UnixStream) -> Result<(), Box<dyn Error>> {
loop {
let mut data = vec![0; 1024];
socket.readable().await?;
let mut command = String::new();
loop {
match socket.try_read(&mut data) {
Ok(0) => break,
Ok(_n) => {
command.push_str(&String::from_utf8(data.clone()).unwrap().trim());
println!("command: {}", command);
},
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
break;
},
Err(e) => {
return Err(e.into());
}
}
}
socket.writable().await?;
match socket.try_write(&command.into_bytes()) {
Ok(_n) => {
},
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
continue;
},
Err(e) => {
return Err(e.into());
}
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AppInfo<'s> {
version: &'s str,
}
impl<'s> AppInfo<'s> {
fn get_version(&self) -> Option<[u8; 3]> {
let mut a: [u8; 3] = [0; 3];
let mut ver = self.version.split('.');
a[0] = ver.next()?.parse().ok()?;
a[1] = ver.next()?.parse().ok()?;
a[2] = ver.next()?.parse().ok()?;
Some(a)
}
}
static mut app_info: AppInfo = AppInfo {
version: env!("CARGO_PKG_VERSION")
};
#[actix_web::main]
async fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
let mut args = args.iter();
args.next().unwrap();
for arg in args {
match arg.as_str() {
"-v" | "--version" => {
println!("Boarders v{}", unsafe { app_info.version });
std::process::exit(0);
}
_ => {
println!("Unknown argument \"{}\"", arg);
}
}
}
/* quick and dirty logging */
SimpleLogger::new()
.with_module_level("mio::poll", LevelFilter::Off)
.with_module_level("actix_server::worker", LevelFilter::Debug)
.with_module_level("handlebars", LevelFilter::Info)
.init().unwrap();
println!("Initializing...");
/*
* TODO: config files
* TODO: allow host to specify the data dir via a config file
* why? why not? maybe you want multiple servers running on
* the same machine
* TODO: allow host to specify config file via cmd line (overrides default name)
*/
let data_dir = Path::new("data/");
let mut pwd = env::current_dir().unwrap();
/* Getting app version and checking for backwards compatability issues */
let mut app_info_file = data_dir.to_path_buf();
app_info_file.push("app.json");
let mut app_info_file = fs::OpenOptions::new().read(true).write(true).create(true).open(app_info_file).unwrap();
let mut app_info_str = String::new();
app_info_file.read_to_string(&mut app_info_str).unwrap();
let cur_app_info: AppInfo = match serde_json::from_str(&app_info_str) {
Ok(a) => a,
Err(_) => {
println!("Malformed app.json file, backing up then creating new");
let s = unsafe { serde_json::to_string(&app_info).unwrap() };
app_info_file.write_all(s.as_bytes()).unwrap();
unsafe { app_info.clone() }
}
};
let ver = cur_app_info.get_version().unwrap();
match ver[0] {
x if x <= 2 => match ver[1] {
y if y <= 2 => match ver[2] {
0 => {
if Command::new("./compat/2.2.0.sh").output().is_err() {
println!("Error, could not complete compatability upgrade");
} else {
println!("Fixed compat issues for v2.2.0");
}
}
_ => {println!("Invalid version number?")}
}
_ => {println!("Invalid version number?")}
}
_ => {println!("Invalid version number?")}
}
/* open/create the directory where board data is placed */
let board_files: ReadDir = fs::read_dir({
pwd.push(data_dir);
pwd.push("boards/");
&pwd
}).unwrap_or({
fs::create_dir_all(&pwd).unwrap();
fs::read_dir(pwd).unwrap()
});
let mut boards: Vec<Board> = Vec::new();
/*
* Iterate over all the files inside the board data dir and
* open/create them. Will panic! if there is an error opening
* a board
*/
for file in board_files.map(|x| x.unwrap()) {
if file.file_type().unwrap().is_dir() {
let mut path = file.path().to_path_buf();
path.push("board.json");
println!("loading board: {:?}", path);
boards.push(
Board::open(&path).unwrap_or_else(|e| {
panic!("Could not open board {:?}\n{}", path, e)
})
);
}
}
/* sort boards by id so they display correctly on the board index */
boards.sort_by(|a,b| a.id.cmp(&b.id));
println!("{}", style(format!("Found and loaded {} boards!", boards.len())).green());
let mut board_data = std::path::PathBuf::new();
board_data.push(&data_dir);
let ff = FactoryFactory::new(data_dir);
let (mut bf, mf, um) = ff.build();
board_data.push("boards/");
bf.set_data_dir(board_data);
/* Initialize all the the shared data crap. copy/pasting code time!! */
let web_data = web::Data::new(AppState {
boards: Mutex::new(boards.clone()),
board_factory: Mutex::new(BoardFactory::load_state({
println!("Loading board factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("boards/factory.json");
dd
}).unwrap_or(bf)),
msg_factory: Mutex::new(MessageFactory::load_state({
println!("Loading message factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("message_factory.json");
dd
}).unwrap_or(mf)),
user_manager: Mutex::new(UserManager::load_state({
println!("Loading user manager file...");
let mut dd = data_dir.to_path_buf();
dd.push("users.json");
dd
}).unwrap_or(um))
});
let wb = web_data.clone();
println!("{}", style("Loaded/Created application data files!").green());
println!("Connecting to Unix socket...");
let (tx0, rx0) = oneshot::channel::<bool>();
/* NOTE: More phases will probably exist in the future */
enum AppPhase {
Normal,
Shutdown
}
let (s_tx, mut s_rx) = watch::channel::<AppPhase>(AppPhase::Normal);
let rt = Runtime::new().unwrap();
let _guard = rt.enter();
/* spawn the thread that manages the unix socket */
tokio::spawn(async move {
let p = Path::new("./boarders.sock");
if p.exists() {
std::fs::remove_file(p).unwrap();
}
let socket = UnixListener::bind("./boarders.sock").unwrap_or_else(|_| {
panic!("{}", style("Could not connect to Unix socket").red());
});
println!("{}", style("Listening on Unix socket!").green());
if tx0.send(true).is_err() {
panic!("{}", style("[FATAL] Thread reciever was dropped! (should never happen)").red().bold());
}
/*
* HACK: clone the reciever a bunch of times to get around moves
* not sure if there's a better way
*/
let s_rx_0 = s_rx.clone();
let spawn_t = tokio::spawn(async move {
let s_rx = s_rx_0.clone();
loop {
let mut s_rx = s_rx.clone();
/* accept socket then wait for exit, or shutdown signal */
let (conn, _addr) = socket.accept().await.unwrap();
tokio::spawn(async move {
select! {
_socket = socket_loop(&conn) => 0,
_state = s_rx.changed() => 1
}
});
}
});
/*
* FIXME: use enums when I feel like it (not urgent)
*/
match select! {
_st = spawn_t => 0,
state = s_rx.changed() => if state.is_ok() {
match *s_rx.borrow() {
AppPhase::Shutdown => 1,
_ => 0
}
} else {
0
}
} {
0 => println!("Client disconnected"),
1 => println!("Shut down client thread(s)"),
_ => println!("Unexpected value from client thread")
}
});
/* why do I want a special message for that? Honestly I don't know... */
rx0.await.unwrap_or_else(
|_| panic!("{}", style("[FATAL] Thread transmitter was dropped! (should never happen)").red().bold())
);
println!("{}", style("Finished initialization").green().bold());
println!("Starting server...");
let server = HttpServer::new(move || {
App::new()
.data(ThreadData {
handlebars: {
let mut h = Handlebars::new();
/*
* TODO: allow user to specify wether or not templates should be
* loaded dynamically or statically, probably via environment
* variable at compile time
*/
/* register handlebars partials here */
handlebars_helper!(msg_helper: |s: str| format!("helped: {}", s.to_string()));
h.register_helper("msg", Box::new(msg_helper));
h.register_partial("head", include_str!("res/snippets/head.hbs")).unwrap();
h.register_partial("login_status", include_str!("res/snippets/login_status.hbs")).unwrap();
h.register_partial("board_bar", include_str!("res/snippets/board_bar.hbs")).unwrap();
/* register handlebars templates here */
h.register_template_string("board_index", include_str!("res/board_index.hbs")).unwrap();
h.register_template_string("welcome", include_str!("res/welcome.hbs")).unwrap();
h.register_template_string("auth", include_str!("res/auth.hbs")).unwrap();
h.register_template_string("board", include_str!("res/board.hbs")).unwrap();
h
}
}).app_data(web_data.clone())
.service(index)
.service(debug)
.service(list_boards)
.service(login)
.service(auth_html)
.service(sign_up)
.service(sign_up_result)
.service(new_board)
.service(new_board_result)
.service(get_board)
.service(board_post)
.service(
/* add all api services under the /api/ scope */
web::scope("/api")
.service(api::boards::new)
.service(api::boards::list)
.service(api::messages::new)
.service(api::users::new)
.service(api::users::list)
.service(api::users::get)
.service(api::users::auth))
/* serve all static files inside ../static/ */
.service(actix_files::Files::new("/static/", "./static/").show_files_listing())
}).bind("127.0.0.1:8080")?.run();
println!("Started server");
println!("{}v{}", include_str!("res/banner"), env!("CARGO_PKG_VERSION"));
/*
* Program will block here until it is Ctrl-C 'd on the command line
* TODO: Block until cmd line exit OR client shutdown command
*/
let res = server.await;
println!("\nExiting...");
/*
* Note to self: handle all errors as much as possible
* Use unwrap() as little as possible
* If anything goes wrong while saving data data WILL get lost
*/
println!("Saving application state...");
for b in wb.boards.lock().unwrap().iter() {
fs::create_dir_all(&b.path).unwrap_or_else(
|x| { println!("Could not create directory {:?}: {}", b.path, x) }
);
b.save_state("board.json").unwrap_or_else(
|x| { println!("Could not save board {:?}: {}", b.title, x) }
);
}
wb.board_factory
.lock()
.unwrap()
.save_state("factory.json")
.unwrap_or_else(
|x| { println!("Could not save user factory state: {}", x) }
);
wb.msg_factory
.lock()
.unwrap()
.save_state("message_factory.json")
.unwrap_or_else(
|x| { println!("Could not save message factory state: {}", x) }
);
wb.user_manager
.lock()
.unwrap()
.save_state("users.json")
.unwrap_or_else(
|x| { println!("Could not save user data: {}", x) }
);
/* send shutdown signal to all unix socket threads */
println!("Shutting down client threads...");
if s_tx.send(AppPhase::Shutdown).is_err() {
println!("{}", style("Could not update the app phase to Shutdown").red().bold());
}
rt.shutdown_timeout(std::time::Duration::from_secs(10));
println!("Done!");
res
}

6
v1/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
target
data
Cargo.lock

480
v1/src/main.rs Normal file
View File

@ -0,0 +1,480 @@
use std::{
env,
error::Error,
fs,
fs::ReadDir,
io,
io::{Read, Write},
path::Path,
process::Command,
sync::Mutex,
};
use actix_web::{get, web, App, HttpResponse, HttpServer, Responder};
mod lib;
use lib::{
boards::{Board, BoardFactory},
messages::MessageFactory,
users::UserManager,
Archiveable, *,
};
mod api;
use api::types::AppState;
mod html;
use html::{accounts::*, boards::*};
use tokio::{
net::{UnixListener, UnixStream},
runtime::Runtime,
select,
sync::{oneshot, watch},
};
use console::style;
use handlebars::{handlebars_helper, Handlebars};
use maplit::btreemap;
use log::LevelFilter;
use simple_logger::SimpleLogger;
use serde::{Deserialize, Serialize};
pub struct ThreadData<'a> {
handlebars: Handlebars<'a>,
}
#[get("/")]
async fn index(tdata: web::Data<ThreadData<'_>>) -> impl Responder {
HttpResponse::Ok().body(
tdata
.handlebars
.render(
"welcome",
&btreemap!(
"version" => env!("CARGO_PKG_VERSION").to_string()
),
)
.unwrap(),
)
}
#[get("/debug/")]
async fn debug(data: web::Data<AppState>) -> impl Responder {
HttpResponse::Ok().body(serde_json::to_string_pretty(data.as_ref()).unwrap())
}
async fn socket_loop(socket: &UnixStream) -> Result<(), Box<dyn Error>> {
loop {
let mut data = vec![0; 1024];
socket.readable().await?;
let mut command = String::new();
loop {
match socket.try_read(&mut data) {
Ok(0) => break,
Ok(_n) => {
command.push_str(String::from_utf8(data.clone()).unwrap().trim());
println!("command: {}", command);
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
break;
}
Err(e) => {
return Err(e.into());
}
}
}
socket.writable().await?;
match socket.try_write(&command.into_bytes()) {
Ok(_n) => {}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
continue;
}
Err(e) => {
return Err(e.into());
}
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct AppInfo<'s> {
version: &'s str,
}
impl<'s> AppInfo<'s> {
fn get_version(&self) -> Option<[u8; 3]> {
let mut a: [u8; 3] = [0; 3];
let mut ver = self.version.split('.');
a[0] = ver.next()?.parse().ok()?;
a[1] = ver.next()?.parse().ok()?;
a[2] = ver.next()?.parse().ok()?;
Some(a)
}
}
static mut APP_INFO: AppInfo = AppInfo {
version: env!("CARGO_PKG_VERSION"),
};
#[actix_web::main]
async fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
let mut args = args.iter();
args.next().unwrap();
for arg in args {
match arg.as_str() {
"-v" | "--version" => {
println!("Boarders v{}", unsafe { APP_INFO.version });
std::process::exit(0);
}
_ => {
println!("Unknown argument \"{}\"", arg);
}
}
}
/* quick and dirty logging */
SimpleLogger::new()
.with_module_level("mio::poll", LevelFilter::Off)
.with_module_level("actix_server::worker", LevelFilter::Debug)
.with_module_level("handlebars", LevelFilter::Info)
.init()
.unwrap();
println!("Initializing...");
/*
* TODO: config files
* TODO: allow host to specify the data dir via a config file
* why? why not? maybe you want multiple servers running on
* the same machine
* TODO: allow host to specify config file via cmd line (overrides default name)
*/
let data_dir = Path::new("data/");
let mut pwd = env::current_dir().unwrap();
/* Getting app version and checking for backwards compatability issues */
let mut app_info_file = data_dir.to_path_buf();
app_info_file.push("app.json");
let mut app_info_file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(app_info_file)
.unwrap();
let mut app_info_str = String::new();
app_info_file.read_to_string(&mut app_info_str).unwrap();
let cur_app_info: AppInfo = match serde_json::from_str(&app_info_str) {
Ok(a) => a,
Err(_) => {
println!("Malformed app.json file, backing up then creating new");
let s = unsafe { serde_json::to_string(&APP_INFO).unwrap() };
app_info_file.write_all(s.as_bytes()).unwrap();
unsafe { APP_INFO.clone() }
}
};
let ver = cur_app_info.get_version().unwrap();
match ver[0] {
x if x <= 2 => match ver[1] {
y if y <= 2 => match ver[2] {
0 => {
if Command::new("./compat/2.2.0.sh").output().is_err() {
println!("Error, could not complete compatability upgrade");
} else {
println!("Fixed compat issues for v2.2.0");
}
}
_ => {
println!("Invalid version number?")
}
},
_ => {
println!("Invalid version number?")
}
},
_ => {
println!("Invalid version number?")
}
}
/* open/create the directory where board data is placed */
let board_files: ReadDir = fs::read_dir({
pwd.push(data_dir);
pwd.push("boards/");
&pwd
})
.unwrap_or({
fs::create_dir_all(&pwd).unwrap();
fs::read_dir(pwd).unwrap()
});
let mut boards: Vec<Board> = Vec::new();
/*
* Iterate over all the files inside the board data dir and
* open/create them. Will panic! if there is an error opening
* a board
*/
for file in board_files.map(|x| x.unwrap()) {
if file.file_type().unwrap().is_dir() {
let mut path = file.path().to_path_buf();
path.push("board.json");
println!("loading board: {:?}", path);
boards.push(
Board::open(&path)
.unwrap_or_else(|e| panic!("Could not open board {:?}\n{}", path, e)),
);
}
}
/* sort boards by id so they display correctly on the board index */
boards.sort_by(|a, b| a.id.cmp(&b.id));
println!(
"{}",
style(format!("Found and loaded {} boards!", boards.len())).green()
);
let mut board_data = std::path::PathBuf::new();
board_data.push(data_dir);
let ff = FactoryFactory::new(data_dir);
let (mut bf, mf, um) = ff.build();
board_data.push("boards/");
bf.set_data_dir(board_data);
/* Initialize all the the shared data crap. copy/pasting code time!! */
let web_data = web::Data::new(AppState {
boards: Mutex::new(boards.clone()),
board_factory: Mutex::new(
BoardFactory::load_state({
println!("Loading board factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("boards/factory.json");
dd
})
.unwrap_or(bf),
),
msg_factory: Mutex::new(
MessageFactory::load_state({
println!("Loading message factory file...");
let mut dd = data_dir.to_path_buf();
dd.push("message_factory.json");
dd
})
.unwrap_or(mf),
),
user_manager: Mutex::new(
UserManager::load_state({
println!("Loading user manager file...");
let mut dd = data_dir.to_path_buf();
dd.push("users.json");
dd
})
.unwrap_or(um),
),
});
let wb = web_data.clone();
println!(
"{}",
style("Loaded/Created application data files!").green()
);
println!("Connecting to Unix socket...");
let (tx0, rx0) = oneshot::channel::<bool>();
/* NOTE: More phases will probably exist in the future */
enum AppPhase {
Normal,
Shutdown,
}
let (s_tx, mut s_rx) = watch::channel::<AppPhase>(AppPhase::Normal);
let rt = Runtime::new().unwrap();
let _guard = rt.enter();
/* spawn the thread that manages the unix socket */
tokio::spawn(async move {
let p = Path::new("./boarders.sock");
if p.exists() {
std::fs::remove_file(p).unwrap();
}
let socket = UnixListener::bind("./boarders.sock").unwrap_or_else(|_| {
panic!("{}", style("Could not connect to Unix socket").red());
});
println!("{}", style("Listening on Unix socket!").green());
if tx0.send(true).is_err() {
panic!(
"{}",
style("[FATAL] Thread reciever was dropped! (should never happen)")
.red()
.bold()
);
}
/*
* HACK: clone the reciever a bunch of times to get around moves
* not sure if there's a better way
*/
let s_rx_0 = s_rx.clone();
let spawn_t = tokio::spawn(async move {
let s_rx = s_rx_0.clone();
loop {
let mut s_rx = s_rx.clone();
/* accept socket then wait for exit, or shutdown signal */
let (conn, _addr) = socket.accept().await.unwrap();
tokio::spawn(async move {
select! {
_socket = socket_loop(&conn) => 0,
_state = s_rx.changed() => 1
}
});
}
});
/*
* FIXME: use enums when I feel like it (not urgent)
*/
match select! {
_st = spawn_t => 0,
state = s_rx.changed() => if state.is_ok() {
match *s_rx.borrow() {
AppPhase::Shutdown => 1,
_ => 0
}
} else {
0
}
} {
0 => println!("Client disconnected"),
1 => println!("Shut down client thread(s)"),
_ => println!("Unexpected value from client thread"),
}
});
/* why do I want a special message for that? Honestly I don't know... */
rx0.await.unwrap_or_else(|_| {
panic!(
"{}",
style("[FATAL] Thread transmitter was dropped! (should never happen)")
.red()
.bold()
)
});
println!("{}", style("Finished initialization").green().bold());
println!("Starting server...");
let server =
HttpServer::new(move || {
App::new()
.data(ThreadData {
handlebars: {
let mut h = Handlebars::new();
/*
* TODO: allow user to specify wether or not templates should be
* loaded dynamically or statically, probably via environment
* variable at compile time
*/
/* register handlebars partials here */
handlebars_helper!(msg_helper: |s: str| format!("helped: {}", s.to_string()));
h.register_helper("msg", Box::new(msg_helper));
h.register_partial("head", include_str!("res/snippets/head.hbs")).unwrap();
h.register_partial("login_status", include_str!("res/snippets/login_status.hbs")).unwrap();
h.register_partial("board_bar", include_str!("res/snippets/board_bar.hbs")).unwrap();
/* register handlebars templates here */
h.register_template_string("board_index", include_str!("res/board_index.hbs")).unwrap();
h.register_template_string("welcome", include_str!("res/welcome.hbs")).unwrap();
h.register_template_string("auth", include_str!("res/auth.hbs")).unwrap();
h.register_template_string("board", include_str!("res/board.hbs")).unwrap();
h
}
}).app_data(web_data.clone())
.service(index)
.service(debug)
.service(list_boards)
.service(login)
.service(auth_html)
.service(sign_up)
.service(sign_up_result)
.service(new_board)
.service(new_board_result)
.service(get_board)
.service(board_post)
.service(
/* add all api services under the /api/ scope */
web::scope("/api")
.service(api::boards::new)
.service(api::boards::list)
.service(api::messages::new)
.service(api::users::new)
.service(api::users::list)
.service(api::users::get)
.service(api::users::auth))
/* serve all static files inside ../static/ */
.service(actix_files::Files::new("/static/", "./static/").show_files_listing())
})
.bind("127.0.0.1:8080")?
.run();
println!("Started server");
println!(
"{}v{}",
include_str!("res/banner"),
env!("CARGO_PKG_VERSION")
);
/*
* Program will block here until it is Ctrl-C 'd on the command line
* TODO: Block until cmd line exit OR client shutdown command
*/
let res = server.await;
println!("\nExiting...");
/*
* Note to self: handle all errors as much as possible
* Use unwrap() as little as possible
* If anything goes wrong while saving data data WILL get lost
*/
println!("Saving application state...");
for b in wb.boards.lock().unwrap().iter() {
fs::create_dir_all(&b.path)
.unwrap_or_else(|x| println!("Could not create directory {:?}: {}", b.path, x));
b.save_state("board.json")
.unwrap_or_else(|x| println!("Could not save board {:?}: {}", b.title, x));
}
wb.board_factory
.lock()
.unwrap()
.save_state("factory.json")
.unwrap_or_else(|x| println!("Could not save user factory state: {}", x));
wb.msg_factory
.lock()
.unwrap()
.save_state("message_factory.json")
.unwrap_or_else(|x| println!("Could not save message factory state: {}", x));
wb.user_manager
.lock()
.unwrap()
.save_state("users.json")
.unwrap_or_else(|x| println!("Could not save user data: {}", x));
/* send shutdown signal to all unix socket threads */
println!("Shutting down client threads...");
if s_tx.send(AppPhase::Shutdown).is_err() {
println!(
"{}",
style("Could not update the app phase to Shutdown")
.red()
.bold()
);
}
rt.shutdown_timeout(std::time::Duration::from_secs(10));
println!("Done!");
res
}

1
v2/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
boarders.v2

3
v2/go.mod Normal file
View File

@ -0,0 +1,3 @@
module boarders.v2
go 1.22.0

9
v2/main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello Boarders!")
}