Compare commits

...

25 Commits
main ... server

Author SHA1 Message Date
Robert Bendun
d2f7562f05 Log found hosts 2023-01-06 09:01:52 +01:00
Robert Bendun
a08389b60f Don't allow Go to leak personal information 2023-01-06 08:01:34 +01:00
Robert Bendun
3183786271 Support OS assining random port for given Musique instance 2023-01-02 07:23:30 +01:00
Robert Bendun
6588363fa3 Fix config path resolution on Windows 2023-01-02 06:41:58 +01:00
Robert Bendun
37bc4e87b3 Musique - mDNS resolution integration 2023-01-02 06:34:54 +01:00
Robert Bendun
95a28723fd Using mDNS to discover remotes 2023-01-02 05:55:48 +01:00
Robert Bendun
dd50882b20 Older gcc doesn't provide operator<< for std::chrono::duration 2022-12-18 20:05:43 +01:00
d4ef97c0c2 del unused pieces 2022-12-17 17:02:58 +01:00
Robert Bendun
c23eae555c musique<->server integration 2022-12-16 16:34:52 +01:00
Robert Bendun
ffc65e0f06 Musique configuration file 2022-12-16 11:38:05 +01:00
Robert Bendun
40ef949dbe integrated new host recognition algorithm with Musique 2022-12-16 02:11:43 +01:00
c49f7ade65 Known hosts knowladge sharing algorithm; nicks and ports 2022-12-15 00:28:36 +01:00
Robert Bendun
67c688d772 cleanup of server directory structure 2022-12-14 17:49:14 +01:00
Robert Bendun
5409aac138 server needs to be before connection 2022-12-09 16:12:33 +01:00
Robert Bendun
6bbb296bb2 Musique-server integration 2022-12-09 16:05:25 +01:00
5e67bf9a2f fix 2022-12-09 15:48:51 +01:00
32765f9e2d fix 2022-12-09 15:34:55 +01:00
c4098de5b4 timesync 2022-12-09 15:04:41 +01:00
Robert Bendun
a56731085d cross platform build of server and Musique 2022-12-09 12:22:50 +01:00
Robert Bendun
3f0014bb2c fix timesync; concurrent scan 2022-12-06 10:20:10 +01:00
3f82212d27 scanner le fix hardcoded ips 2022-12-05 22:54:46 +01:00
Robert Bendun
df68d8fa9d Bodged Musique & server integration 2022-12-05 22:34:47 +01:00
14cddb8051 timesync wip 2022-12-05 21:50:30 +01:00
5eae7b3019 scanner fix, minor changes 2022-11-29 21:15:39 +01:00
80830ac363 Migrated from https://git.wmi.amu.edu.pl/s416496/go-http-server 2022-11-23 22:29:10 +01:00
29 changed files with 1105 additions and 23 deletions

View File

@ -5,4 +5,4 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt update && apt upgrade -y && apt install -y build-essential software-properties-common zip unzip
RUN add-apt-repository ppa:ubuntu-toolchain-r/test
RUN apt install -y gcc-11 g++-11 libasound2-dev
RUN apt install -y gcc-11 g++-11 libasound2-dev golang

View File

@ -48,3 +48,4 @@ doc/wprowadzenie.html: doc/wprowadzenie.md
$(shell mkdir -p $(subst musique/,bin/$(os)/,$(shell find musique/* -type d)))
$(shell mkdir -p $(subst musique/,bin/$(os)/debug/,$(shell find musique/* -type d)))
$(shell mkdir -p bin/$(os)/server/)

View File

@ -1,16 +1,17 @@
MAKEFLAGS="-j $(grep -c ^processor /proc/cpuinfo)"
CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch -Werror=return-type -Werror=unused-result
CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -I. -Ilib/bestline/ -Ilib/rtmidi/
LDFLAGS=-flto
LDLIBS= -lpthread
RELEASE_FLAGS=-O2
DEBUG_FLAGS=-O0 -ggdb -fsanitize=undefined -DDebug
ifeq ($(shell uname),Darwin)
os=macos
else
os=linux
endif
CXXFLAGS:=$(CXXFLAGS) -std=c++20 -Wall -Wextra -Werror=switch -Werror=return-type -Werror=unused-result
CPPFLAGS:=$(CPPFLAGS) -Ilib/expected/ -I. -Ilib/bestline/ -Ilib/rtmidi/ -Ibin/$(os)/server/
LDFLAGS=-flto
LDLIBS= -lpthread
RELEASE_FLAGS=-O2
DEBUG_FLAGS=-O0 -ggdb -fsanitize=undefined -DDebug

1
lib/midi Submodule

@ -0,0 +1 @@
Subproject commit f42b663f0d08fc629c7deb26cd32ee06fba76d83

94
musique/config.cc Normal file
View File

@ -0,0 +1,94 @@
#include <fstream>
#include <iterator>
#include <musique/config.hh>
#include <musique/errors.hh>
#include <musique/platform.hh>
#include <iostream>
#ifdef MUSIQUE_WINDOWS
#include <shlobj.h>
#include <knownfolders.h>
#include <cstring>
#endif
// FIXME Move trim to some header
extern void trim(std::string_view &s);
// FIXME Report errors when parsing fails
config::Sections config::from_file(std::string const& path)
{
Sections sections;
Key_Value *kv = &sections[""];
std::ifstream in(path);
for (std::string linebuf; std::getline(in, linebuf); ) {
std::string_view line = linebuf;
trim(line);
if (auto comment = line.find('#'); comment != std::string_view::npos) {
line = line.substr(0, comment);
}
if (line.starts_with('[') && line.ends_with(']')) {
kv = &sections[std::string(line.begin()+1, line.end()-1)];
continue;
}
if (auto split = line.find('='); split != std::string_view::npos) {
auto key = line.substr(0, split);
auto val = line.substr(split+1);
trim(key);
trim(val);
(*kv)[std::string(key)] = std::string(val);
}
}
return sections;
}
void config::to_file(std::string const& path, Sections const& sections)
{
std::ofstream out(path, std::ios_base::trunc | std::ios_base::out);
if (!out.is_open()) {
std::cout << "Failed to save configuration at " << path << std::endl;
return;
}
for (auto const& [section, kv] : sections) {
out << '[' << section << "]\n";
for (auto const& [key, val] : kv) {
out << key << " = " << val << '\n';
}
}
}
std::string config::location()
{
#ifdef MUSIQUE_LINUX
if (auto config_dir = getenv("XDG_CONFIG_HOME")) {
return config_dir + std::string("/musique.conf");
} else if (auto home_dir = getenv("HOME")) {
return std::string(home_dir) + "/.config/musique.conf";
} else {
unimplemented("Neither XDG_CONFIG_HOME nor HOME enviroment variable are defined");
}
#endif
#ifdef MUSIQUE_WINDOWS
auto drive = getenv("HOMEDRIVE");
auto path = getenv("HOMEPATH");
ensure(drive && path, "Windows failed to provide HOMEDRIVE & HOMEPATH variables");
return std::string(drive) + std::string(path) + "\\Documents\\musique.conf";
#endif
#ifdef MUSIQUE_DARWIN
if (auto home_dir = getenv("HOME")) {
return std::string(home_dir) + "/Library/Application Support/musique.conf";
} else {
unimplemented("HOME enviroment variable is not defined");
}
#endif
unimplemented();
}

20
musique/config.hh Normal file
View File

@ -0,0 +1,20 @@
#ifndef MUSIQUE_CONFIG_HH
#define MUSIQUE_CONFIG_HH
#include <optional>
#include <string>
#include <unordered_map>
// Parses INI files
namespace config
{
using Key_Value = std::unordered_map<std::string, std::string>;
using Sections = std::unordered_map<std::string, Key_Value>;
Sections from_file(std::string const& path);
void to_file(std::string const& path, Sections const& sections);
std::string location();
}
#endif // MUSIQUE_CONFIG_HH

View File

@ -4,6 +4,8 @@
#include <musique/interpreter/interpreter.hh>
#include <musique/try.hh>
#include <server.h>
#include <random>
#include <memory>
#include <iostream>
@ -1109,6 +1111,27 @@ static Result<Value> builtin_call(Interpreter &i, std::vector<Value> args)
return callable(i, std::move(args));
}
static Result<Value> builtin_start(Interpreter &interpreter, std::span<Ast> args)
{
Value ret{};
auto begin = std::chrono::steady_clock::now();
ServerBeginProtocol();
for (auto const& ast : args) {
ret = Try(interpreter.eval((Ast)ast));
}
auto end = std::chrono::steady_clock::now();
std::cout << "Start took "
<< std::chrono::duration_cast<std::chrono::duration<float>>(end - begin).count()
<< "s" << std::endl;
return ret;
}
void Interpreter::register_builtin_functions()
{
auto &global = *Env::global;
@ -1152,6 +1175,7 @@ void Interpreter::register_builtin_functions()
global.force_define("shuffle", builtin_shuffle);
global.force_define("sim", builtin_sim);
global.force_define("sort", builtin_sort);
global.force_define("start", builtin_start);
global.force_define("try", builtin_try);
global.force_define("typeof", builtin_typeof);
global.force_define("uniq", builtin_uniq);

View File

@ -17,6 +17,9 @@
#include <musique/try.hh>
#include <musique/unicode.hh>
#include <musique/value/block.hh>
#include <musique/config.hh>
#include <server.h>
#ifdef _WIN32
extern "C" {
@ -35,6 +38,8 @@ static bool quiet_mode = false;
static bool ast_only_mode = false;
static bool enable_repl = false;
static unsigned repl_line_number = 1;
static std::string nick;
static int port;
#define Ignore(Call) do { auto const ignore_ ## __LINE__ = (Call); (void) ignore_ ## __LINE__; } while(0)
@ -101,11 +106,13 @@ void print_repl_help()
":!<command> - allows for execution of any shell command\n"
":clear - clears screen\n"
":load <file> - loads file into Musique session\n"
":discover - tries to discover new remotes\n"
":remotes - list all known remotes\n"
;
}
/// Trim spaces from left an right
static void trim(std::string_view &s)
void trim(std::string_view &s)
{
// left trim
if (auto const i = std::find_if_not(s.begin(), s.end(), unicode::is_space); i != s.begin()) {
@ -171,6 +178,11 @@ struct Runner
ensure(the == nullptr, "Only one instance of runner is supported");
the = this;
GoInt goport = port;
GoString gonick { .p = nick.data(), .n = ptrdiff_t(nick.size()) };
ServerInit(gonick, goport);
interpreter.midi_connection = &midi;
if (output_port) {
midi.connect_output(*output_port);
@ -197,6 +209,16 @@ struct Runner
std::cout << std::endl;
return {};
});
Env::global->force_define("timeout", +[](Interpreter&, std::vector<Value> args) -> Result<Value> {
if (auto a = match<Number>(args); a) {
auto [timeout] = *a;
auto gotimeout = GoInt64((timeout * Number(1000)).floor().as_int());
SetTimeout(gotimeout);
return Value{};
}
unimplemented();
});
}
Runner(Runner const&) = delete;
@ -330,6 +352,21 @@ static Result<bool> handle_repl_session_commands(std::string_view input, Runner
return std::nullopt;
}
},
Command {
"remotes",
+[](Runner&, std::optional<std::string_view>) -> std::optional<Error> {
ListKnownRemotes();
return std::nullopt;
}
},
Command {
"discover",
+[](Runner&, std::optional<std::string_view>) -> std::optional<Error> {
Discover();
ListKnownRemotes();
return std::nullopt;
}
},
};
if (input.starts_with('!')) {
@ -441,6 +478,37 @@ static std::optional<Error> Main(std::span<char const*> args)
std::exit(1);
}
bool save_new_config = false;
// TODO Write configuration
// TODO Nicer configuration interface (maybe paths?)
auto config = config::from_file(config::location());
if (config.contains("net") && config["net"].contains("nick")) {
nick = config["net"]["nick"];
} else {
std::cout << "Please enter your nick: ";
std::getline(std::cin, nick);
auto nick_view = std::string_view(nick);
trim(nick_view); // FIXME Trim in place
nick = nick_view;
config["net"]["nick"] = nick;
save_new_config = true;
}
if (config.contains("net") && config["net"].contains("port")) {
auto const& port_str = config["net"]["port"];
// FIXME Handle port number parsing errors
std::from_chars(port_str.data(), port_str.data() + port_str.size(), port);
} else {
port = 0;
config["net"]["port"] = std::to_string(port);
save_new_config = true;
}
if (save_new_config) {
config::to_file(config::location(), config);
}
Runner runner{output_port};
for (auto const& [type, argument] : runnables) {

22
musique/platform.hh Normal file
View File

@ -0,0 +1,22 @@
#ifndef MUSIQUE_PLATFORM_HH
#define MUSIQUE_PLATFORM_HH
enum class Platform
{
Darwin,
Linux,
Windows,
};
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
static constexpr Platform Current_Platform = Platform::Windows;
#define MUSIQUE_WINDOWS
#elif __APPLE__
static constexpr Platform Current_Platform = Platform::Darwin;
#define MUSIQUE_DARWIN
#else
static constexpr Platform Current_Platform = Platform::Linux;
#define MUSIQUE_LINUX
#endif
#endif // MUSIQUE_PLATFORM_HH

View File

@ -1,22 +1,28 @@
Release_Obj=$(addprefix bin/$(os)/,$(Obj))
Server=bin/$(os)/server/server.h bin/$(os)/server/server.o
$(Server) &: server/*.go server/**/*.go
cd server/; GOOS="$(GOOS)" GOARCH="$(GOARCH)" CGO_ENABLED=1 CC="$(CC)" \
go build -ldflags="-s -w" -trimpath -o ../bin/$(os)/server/server.o -buildmode=c-archive
bin/$(os)/bestline.o: lib/bestline/bestline.c lib/bestline/bestline.h
@echo "CC $@"
@$(CC) $< -c -O3 -o $@
bin/$(os)/%.o: musique/%.cc
bin/$(os)/%.o: musique/%.cc $(Server)
@echo "CXX $@"
@$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $< -c
bin/$(os)/$(Target): $(Release_Obj) bin/$(os)/main.o bin/$(os)/rtmidi.o $(Bestline)
bin/$(os)/$(Target): $(Release_Obj) bin/$(os)/main.o bin/$(os)/rtmidi.o $(Bestline) $(Server)
@echo "CXX $@"
@$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $(Release_Obj) bin/$(os)/rtmidi.o $(Bestline) $(LDFLAGS) $(LDLIBS)
@$(CXX) $(CXXFLAGS) $(RELEASE_FLAGS) $(CPPFLAGS) -o $@ $(Release_Obj) bin/$(os)/rtmidi.o $(Bestline) $(LDFLAGS) $(LDLIBS) bin/$(os)/server/server.o
Debug_Obj=$(addprefix bin/$(os)/debug/,$(Obj))
bin/$(os)/debug/$(Target): $(Debug_Obj) bin/$(os)/debug/main.o bin/$(os)/rtmidi.o $(Bestline)
bin/$(os)/debug/$(Target): $(Debug_Obj) bin/$(os)/debug/main.o bin/$(os)/rtmidi.o $(Bestline) $(Server)
@echo "CXX $@"
@$(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) $(CPPFLAGS) -o $@ $(Debug_Obj) bin/$(os)/rtmidi.o $(Bestline) $(LDFLAGS) $(LDLIBS)
@$(CXX) $(CXXFLAGS) $(DEBUG_FLAGS) $(CPPFLAGS) -o $@ $(Debug_Obj) bin/$(os)/rtmidi.o $(Bestline) $(LDFLAGS) $(LDLIBS) bin/$(os)/server/server.o
bin/$(os)/debug/%.o: musique/%.cc
@echo "CXX $@"

View File

@ -4,3 +4,5 @@ CPPFLAGS:=$(CPPFLAGS) -D __LINUX_ALSA__
LDLIBS:=-lasound $(LDLIBS) -static-libgcc -static-libstdc++
Bestline=bin/$(os)/bestline.o
Target=musique
GOOS=linux
GOARCH=amd64

View File

@ -5,3 +5,5 @@ LDLIBS:=-framework CoreMIDI -framework CoreAudio -framework CoreFoundation $(LDL
Release_Obj=$(addprefix bin/,$(Obj))
Bestline=bin/$(os)/bestline.o
Target=musique
GOOS=darwin
GOARCH=amd64

View File

@ -4,7 +4,7 @@
# Release is defined as a zip archive containing source code,
# build binaries for supported platforms and build documentation
set -e -o pipefail
set -xe -o pipefail
Suffix="$(date +"%Y-%m-%d")"
Target="release_$Suffix"
@ -16,11 +16,9 @@ fi
mkdir -p "$Target"
if [[ "$(docker images -q "$Image")" == "" ]]; then
docker build -t "$Image" .
fi
sudo rm -rf bin/
docker run -it --rm -v "$(pwd):/musique" -w /musique "$Image" make os=linux CC=gcc-11 CXX=g++-11 >/dev/null
# make os=linux CC=gcc CXX=g++ >/dev/null
make os=linux CC=gcc-11 CXX=g++-11 >/dev/null
cp bin/musique "$Target"/musique-x86_64-linux

View File

@ -1,5 +1,7 @@
CC=i686-w64-mingw32-gcc
CXX=i686-w64-mingw32-g++
CC=x86_64-w64-mingw32-cc
CXX=x86_64-w64-mingw32-c++
CPPFLAGS:=$(CPPFLAGS) -D__WINDOWS_MM__
LDLIBS:=-lwinmm $(LDLIBS) -static-libgcc -static-libstdc++ -static
LDLIBS:=-lwinmm -luuid $(LDLIBS) -static-libgcc -static-libstdc++ -static
Target=musique.exe
GOOS=windows
GOARCH=amd64

2
server/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
test*.sh
server

35
server/README.md Normal file
View File

@ -0,0 +1,35 @@
# Server
## Development notes
### Testing server list synchronisation (2022-12-14)
For ease of testing you can launch N instances of server in N tmux panes.
```console
$ while true; do sleep {n}; echo "======="; ./server -nick {nick} -port {port}; done
```
where `n` is increasing for each server to ensure that initial scan would not cover entire task of network scanning.
Next you can use this script to test:
```bash
go build
killall server
sleep 4
# Repeat line below for all N servers
echo '{"version":1, "type":"hosts"}' | nc localhost {port} | jq
# Choose one or few that will request synchronization with their remotes
echo '{"version":1, "type":"synchronize-hosts-with-remotes"}' | nc localhost {port} | jq
# Ensure that all synchronisation propagated with enough sleep time
sleep 2
# Repeat line below for all N servers
echo '{"version":1, "type":"hosts"}' | nc localhost {port} | jq
```

13
server/go.mod Normal file
View File

@ -0,0 +1,13 @@
module musique/server
go 1.19
require github.com/RobertBendun/zeroconf/v2 v2.0.0-20230102034354-649340f2f3b6
require (
github.com/miekg/dns v1.1.50 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/tools v0.4.0 // indirect
)

45
server/go.sum Normal file
View File

@ -0,0 +1,45 @@
github.com/RobertBendun/zeroconf/v2 v2.0.0-20230102034354-649340f2f3b6 h1:u4H25RhCTadMtZmrvcS5ze8qUOBQ20gAoTJ4qzvp8hs=
github.com/RobertBendun/zeroconf/v2 v2.0.0-20230102034354-649340f2f3b6/go.mod h1:KcFfULkjW8Z9cUQxr3MlM8aZ/SKMB5IRPUiX75nfe88=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

40
server/handlers.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"fmt"
"net/http"
)
func cmdHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/cmd" {
http.Error(w, "404 not found.", http.StatusNotFound)
return
}
if r.Method != "POST" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
if err := r.ParseForm(); err != nil {
fmt.Fprintf(w, "ParseForm() err: %v", err)
return
}
fmt.Fprintf(w, "POST request successful\n")
cmd := r.FormValue("cmd")
fmt.Fprintf(w, "Command = %s\n", cmd)
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/hello" {
http.Error(w, "404 not found.", http.StatusNotFound)
return
}
if r.Method != "GET" {
http.Error(w, "Method is not supported.", http.StatusNotFound)
return
}
fmt.Fprintf(w, "Hello!")
}

319
server/main.go Normal file
View File

@ -0,0 +1,319 @@
package main
import (
"errors"
"flag"
"fmt"
"log"
"musique/server/proto"
"musique/server/router"
"net"
"os"
"strings"
"sync"
"time"
)
func scanError(scanResult []string, conn net.Conn) {
if scanResult == nil {
conn.Write([]byte("Empty scan result, run 'scan' first.\n"))
}
}
type TimeExchange struct {
before, after, remote int64
}
func (e *TimeExchange) estimateFor(host string) bool {
e.before = time.Now().UnixMilli()
var response proto.TimeResponse
if err := proto.Command(host, proto.Time(), &response); err != nil {
log.Printf("failed to send time command: %v\n", err)
return false
}
e.after = time.Now().UnixMilli()
return true
}
func timesync() {
wg := sync.WaitGroup{}
wg.Add(len(remotes))
type response struct {
TimeExchange
key string
}
// Gather time from each host
responses := make(chan response, len(remotes))
for key, remote := range remotes {
key, remote := key, remote
go func() {
defer wg.Done()
exchange := TimeExchange{}
if exchange.estimateFor(remote.Address) {
responses <- response{exchange, key}
}
}()
}
wg.Wait()
close(responses)
for response := range responses {
remote := remotes[response.key]
remote.TimeExchange = response.TimeExchange
}
}
var maxReactionTime = int64(1_000)
func isThisMyAddress(address string) bool {
addrs, err := net.InterfaceAddrs()
if err != nil {
return false
}
for _, addr := range addrs {
ip, _, err := net.ParseCIDR(addr.String())
if err == nil && ip.To4() != nil && fmt.Sprintf("%s:%d", ip, port) == address {
return true
}
}
return false
}
func notifyAll() <-chan time.Time {
wg := sync.WaitGroup{}
wg.Add(len(remotes))
startDeadline := time.After(time.Duration(maxReactionTime) * time.Millisecond)
for _, remote := range remotes {
remote := remote
go func() {
startTime := maxReactionTime - (remote.after-remote.before)/2
var response proto.StartResponse
err := proto.CommandTimeout(
remote.Address,
proto.Start(startTime),
&response,
time.Duration(startTime)*time.Millisecond,
)
if err != nil {
log.Printf("failed to notify %s: %v\n", remote.Address, err)
}
wg.Done()
}()
}
return startDeadline
}
var (
pinger chan struct{}
)
func registerRoutes(r *router.Router) {
r.Add("handshake", func(incoming net.Conn, request proto.Request) interface{} {
var response proto.HandshakeResponse
response.Version = proto.Version
response.Nick = nick
return response
})
r.Add("hosts", func(incoming net.Conn, request proto.Request) interface{} {
var response proto.HostsResponse
for _, remote := range remotes {
response.Hosts = append(response.Hosts, proto.HostsResponseEntry{
Nick: remote.Nick,
Version: remote.Version,
Address: remote.Address,
})
}
return response
})
r.Add("synchronize-hosts", func(incoming net.Conn, request proto.Request) interface{} {
return synchronizeHosts(request.HostsResponse)
})
r.Add("synchronize-hosts-with-remotes", func(incoming net.Conn, request proto.Request) interface{} {
synchronizeHostsWithRemotes()
return nil
})
r.Add("time", func(incoming net.Conn, request proto.Request) interface{} {
return proto.TimeResponse{
Time: time.Now().UnixMilli(),
}
})
pinger = make(chan struct{}, 12)
r.Add("start", func(incoming net.Conn, request proto.Request) interface{} {
go func() {
time.Sleep(time.Duration(request.StartTime) * time.Millisecond)
pinger <- struct{}{}
}()
return proto.StartResponse{
Succeded: true,
}
})
}
type Remote struct {
TimeExchange
Address string
Nick string
Version string
}
var (
baseIP string = ""
nick string
port int = 8888
remotes map[string]*Remote
)
func synchronizeHosts(incoming proto.HostsResponse) (response proto.HostsResponse) {
visitedHosts := make(map[string]struct{})
// Add all hosts that are in incoming to our list of remotes
// Additionaly build set of all hosts that remote knows
for _, incomingHost := range incoming.Hosts {
if _, ok := remotes[incomingHost.Address]; !ok && !isThisMyAddress(incomingHost.Address) {
remotes[incomingHost.Address] = &Remote{
Address: incomingHost.Address,
Nick: incomingHost.Nick,
Version: incomingHost.Version,
}
}
visitedHosts[incomingHost.Address] = struct{}{}
}
// Build list of hosts that incoming doesn't know
for _, remote := range remotes {
if _, ok := visitedHosts[remote.Address]; !ok {
response.Hosts = append(response.Hosts, proto.HostsResponseEntry{
Address: remote.Address,
Version: remote.Version,
Nick: remote.Nick,
})
}
}
return
}
func myAddressInTheSameNetwork(remote string) (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", fmt.Errorf("myAddressInTheSameNetwork: %v", err)
}
remoteInParts := strings.Split(remote, ":")
if len(remoteInParts) == 2 {
remote = remoteInParts[0]
}
remoteIP := net.ParseIP(remote)
if remoteIP == nil {
// TODO Hoist error to global variable
return "", errors.New("Cannot parse remote IP")
}
for _, addr := range addrs {
ip, ipNet, err := net.ParseCIDR(addr.String())
if err == nil && ipNet.Contains(remoteIP) {
return fmt.Sprintf("%s:%d", ip, port), nil
}
}
// TODO Hoist error to global variable
return "", errors.New("Cannot find matching IP addr")
}
func synchronizeHostsWithRemotes() {
previousResponseLength := -1
var response proto.HostsResponse
for previousResponseLength != len(response.Hosts) {
response = proto.HostsResponse{}
// Add all known remotes
for _, remote := range remotes {
response.Hosts = append(response.Hosts, proto.HostsResponseEntry{
Address: remote.Address,
Nick: remote.Nick,
Version: remote.Version,
})
}
// Send constructed list to each remote
previousResponseLength = len(response.Hosts)
for _, remote := range response.Hosts {
var localResponse proto.HostsResponse
localResponse.Hosts = make([]proto.HostsResponseEntry, len(response.Hosts))
copy(localResponse.Hosts, response.Hosts)
myAddress, err := myAddressInTheSameNetwork(remote.Address)
// TODO Report when err != nil
if err == nil {
localResponse.Hosts = append(localResponse.Hosts, proto.HostsResponseEntry{
Address: myAddress,
Nick: nick,
Version: proto.Version,
})
}
var remoteResponse proto.HostsResponse
proto.Command(remote.Address, proto.SynchronizeHosts(localResponse), &remoteResponse)
synchronizeHosts(remoteResponse)
}
}
}
func main() {
var (
logsPath string
)
flag.StringVar(&baseIP, "ip", "", "IP where server will listen")
flag.StringVar(&nick, "nick", "", "Name that is going to be used to recognize this server")
flag.IntVar(&port, "port", 8081, "TCP port where server receives connections")
flag.StringVar(&logsPath, "logs", "", "Target file for logs from server. By default stdout")
flag.Parse()
if len(logsPath) != 0 {
// TODO Is defer logFile.Close() needed here? Dunno
logFile, err := os.OpenFile(logsPath, os.O_WRONLY|os.O_APPEND, 0o640)
if err != nil {
fmt.Fprintf(os.Stderr, "Cannot open log file: %v\n", err)
os.Exit(1)
}
log.SetOutput(logFile)
}
if len(nick) == 0 {
log.Fatalln("Please provide nick via --nick flag")
}
server, err := registerDNS()
if err != nil {
log.Fatalln("Failed to register DNS:", err)
}
defer server.Shutdown()
r := router.Router{}
registerRoutes(&r)
exit, err := r.Run(baseIP, &port)
if err != nil {
log.Fatalln(err)
}
if err := registerRemotes(5); err != nil {
log.Fatalln(err)
}
for range exit {
}
}

100
server/mdns.go Normal file
View File

@ -0,0 +1,100 @@
package main
import (
"context"
"fmt"
"log"
"musique/server/proto"
"net"
"sync"
"time"
"strings"
"github.com/RobertBendun/zeroconf/v2"
)
func doHandshake(wg *sync.WaitGroup, service *zeroconf.ServiceEntry, remotes chan<- Remote, timeout time.Duration) {
for _, ip := range service.AddrIPv4 {
wg.Add(1)
go func(ip net.IP) {
defer wg.Done()
target := fmt.Sprintf("%s:%d", ip, service.Port)
if isThisMyAddress(target) {
return
}
var hs proto.HandshakeResponse
err := proto.CommandTimeout(target, proto.Handshake(), &hs, timeout)
if err == nil {
// log.Println("Received handshake response", target, hs)
remotes <- Remote{
Address: target,
Nick: hs.Nick,
Version: hs.Version,
}
}
}(ip)
}
}
// Register all remotes that cane be found in `waitTime` seconds
func registerRemotes(waitTime int) error {
wg := sync.WaitGroup{}
done := make(chan error, 1)
incomingRemotes := make(chan Remote, 32)
entries := make(chan *zeroconf.ServiceEntry, 12)
timeout := time.Second*time.Duration(waitTime)
wg.Add(1)
go func(results <-chan *zeroconf.ServiceEntry) {
for entry := range results {
log.Println("Found service at", entry.HostName, "at addrs", entry.AddrIPv4)
doHandshake(&wg, entry, incomingRemotes, timeout)
}
wg.Done()
done <- nil
}(entries)
go func() {
wg.Wait()
close(incomingRemotes)
}()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
go func() {
err := zeroconf.Browse(ctx, "_musique._tcp", "local", entries)
if err != nil {
done <- fmt.Errorf("failed to browse: %v", err)
}
<-ctx.Done()
}()
remotes = make(map[string]*Remote)
for remote := range incomingRemotes {
remote := remote
remotes[remote.Address] = &remote
}
msg := &strings.Builder{}
comma := false
for _, remote := range remotes {
if comma {
fmt.Fprint(msg, ", ")
}
fmt.Fprintf(msg, "%s@%s", remote.Nick, remote.Address)
comma = true
}
log.Println("Hosts found:", msg.String())
return <-done
}
type dnsServer interface {
Shutdown()
}
func registerDNS() (dnsServer, error) {
return zeroconf.Register("Musique", "_musique._tcp", "local", port, []string{}, nil)
}

94
server/musique-bridge.go Normal file
View File

@ -0,0 +1,94 @@
package main
import (
"C"
"fmt"
"log"
"musique/server/router"
"os"
"sort"
)
const (
initialWaitingTime = 3
userRequestedWatingingTime = 5
)
//export ServerInit
func ServerInit(inputNick string, inputPort int) {
logFile, err := os.OpenFile("musique.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o640)
if err != nil {
log.Fatalln(err)
}
log.SetOutput(logFile)
nick = inputNick
port = inputPort
r := router.Router{}
registerRoutes(&r)
_, err = r.Run(baseIP, &port)
if err != nil {
fmt.Println("Address already in use. You have probably another instance of Musique running")
log.Fatalln(err)
}
_, err = registerDNS()
if err != nil {
log.Fatalln("Failed to register DNS:", err)
}
// defer server.Shutdown()
if err := registerRemotes(initialWaitingTime); err != nil {
log.Fatalln(err)
}
timesync()
}
//export SetTimeout
func SetTimeout(timeout int64) {
maxReactionTime = timeout
}
//export ServerBeginProtocol
func ServerBeginProtocol() {
self := notifyAll()
select {
case <-self:
case <-pinger:
}
}
//export Discover
func Discover() {
if err := registerRemotes(userRequestedWatingingTime); err != nil {
log.Println("discover:", err)
}
// synchronizeHostsWithRemotes()
}
//export ListKnownRemotes
func ListKnownRemotes() {
type nickAddr struct {
nick, addr string
}
list := []nickAddr{}
for _, remote := range remotes {
list = append(list, nickAddr{remote.Nick, remote.Address})
}
sort.Slice(list, func(i, j int) bool {
if list[i].nick == list[j].nick {
return list[i].addr < list[j].addr
}
return list[i].nick < list[j].nick
})
for _, nickAddr := range list {
fmt.Printf("%s@%s\n", nickAddr.nick, nickAddr.addr)
}
os.Stdout.Sync()
}

10
server/proto/basic.go Normal file
View File

@ -0,0 +1,10 @@
package proto
const Version = "1"
type Request struct {
Version string
Type string
HostsResponse
StartTime int64
}

12
server/proto/handshake.go Normal file
View File

@ -0,0 +1,12 @@
package proto
type HandshakeResponse struct {
Nick string
Version string
}
func Handshake() (req Request) {
req.Type = "handshake"
req.Version = Version
return
}

24
server/proto/hosts.go Normal file
View File

@ -0,0 +1,24 @@
package proto
type HostsResponseEntry struct {
Nick string
Address string
Version string
}
type HostsResponse struct {
Hosts []HostsResponseEntry
}
func Hosts() (req Request) {
req.Version = Version
req.Type = "hosts"
return
}
func SynchronizeHosts(response HostsResponse) (req Request) {
req.HostsResponse = response
req.Version = Version
req.Type = "synchronize-hosts"
return
}

52
server/proto/net.go Normal file
View File

@ -0,0 +1,52 @@
package proto
import (
"encoding/json"
"errors"
"net"
"time"
)
func Command(target string, request interface{}, response interface{}) error {
conn, err := net.Dial("tcp", target)
if err != nil {
return err
}
defer conn.Close()
if err = json.NewEncoder(conn).Encode(request); err != nil {
return err
}
return json.NewDecoder(conn).Decode(response)
}
func CommandTimeout(target string, request interface{}, response interface{}, timeout time.Duration) error {
responseChan := make(chan interface{})
errorChan := make(chan error)
go func() {
conn, err := net.DialTimeout("tcp", target, timeout)
if err != nil {
errorChan <- err
return
}
defer conn.Close()
if err = json.NewEncoder(conn).Encode(request); err != nil {
errorChan <- err
return
}
responseChan <- json.NewDecoder(conn).Decode(response)
}()
select {
case response = <-responseChan:
return nil
case err := <-errorChan:
return err
case <-time.After(timeout):
return errors.New("timout")
}
}

12
server/proto/start.go Normal file
View File

@ -0,0 +1,12 @@
package proto
type StartResponse struct {
Succeded bool
}
func Start(startTime int64) (req Request) {
req.Type = "start"
req.Version = Version
req.StartTime = startTime
return
}

11
server/proto/time.go Normal file
View File

@ -0,0 +1,11 @@
package proto
type TimeResponse struct {
Time int64
}
func Time() (req Request) {
req.Type = "time"
req.Version = Version
return
}

72
server/router/router.go Normal file
View File

@ -0,0 +1,72 @@
package router
import (
"encoding/json"
"musique/server/proto"
"net"
"log"
"fmt"
)
type Route func(incoming net.Conn, request proto.Request) interface{}
type Router struct {
routes map[string]Route
port int
baseIP string
}
func (router *Router) Add(name string, route Route) {
if router.routes == nil {
router.routes = make(map[string]Route)
}
router.routes[name] = route
}
func (router *Router) Run(ip string, port *int) (<-chan struct{}, error) {
router.port = *port
router.baseIP = ip
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", router.baseIP, router.port))
if err != nil {
return nil, err
}
_, actualPort, err := net.SplitHostPort(listener.Addr().String())
if err != nil {
return nil, err
}
fmt.Sscan(actualPort, port)
exit := make(chan struct{})
go func() {
defer listener.Close()
defer close(exit)
for {
incoming, err := listener.Accept()
if err != nil {
log.Println("failed to accept connection:", err)
}
go router.handleIncoming(incoming)
}
}()
return exit, nil
}
func (router *Router) handleIncoming(incoming net.Conn) {
defer incoming.Close()
request := proto.Request{}
json.NewDecoder(incoming).Decode(&request)
// log.Printf("%s: %+v\n", incoming.RemoteAddr(), request)
if handler, ok := router.routes[request.Type]; ok {
if response := handler(incoming, request); response != nil {
json.NewEncoder(incoming).Encode(response)
}
} else {
log.Printf("unrecongized route: %v\n", request.Type)
}
}