Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d2f7562f05 | ||
![]() |
a08389b60f | ||
![]() |
3183786271 | ||
![]() |
6588363fa3 | ||
![]() |
37bc4e87b3 | ||
![]() |
95a28723fd | ||
![]() |
dd50882b20 | ||
d4ef97c0c2 | |||
![]() |
c23eae555c | ||
![]() |
ffc65e0f06 | ||
![]() |
40ef949dbe | ||
c49f7ade65 | |||
![]() |
67c688d772 | ||
![]() |
5409aac138 | ||
![]() |
6bbb296bb2 | ||
5e67bf9a2f | |||
32765f9e2d | |||
c4098de5b4 | |||
![]() |
a56731085d | ||
![]() |
3f0014bb2c | ||
3f82212d27 | |||
![]() |
df68d8fa9d | ||
14cddb8051 | |||
5eae7b3019 | |||
80830ac363 |
@ -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
|
||||
|
1
Makefile
1
Makefile
@ -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/)
|
||||
|
17
config.mk
17
config.mk
@ -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
1
lib/midi
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit f42b663f0d08fc629c7deb26cd32ee06fba76d83
|
94
musique/config.cc
Normal file
94
musique/config.cc
Normal 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 = §ions[""];
|
||||
|
||||
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 = §ions[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
20
musique/config.hh
Normal 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
|
@ -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);
|
||||
|
@ -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
22
musique/platform.hh
Normal 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
|
@ -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 $@"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
2
server/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
test*.sh
|
||||
server
|
35
server/README.md
Normal file
35
server/README.md
Normal 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
13
server/go.mod
Normal 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
45
server/go.sum
Normal 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
40
server/handlers.go
Normal 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
319
server/main.go
Normal 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
100
server/mdns.go
Normal 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
94
server/musique-bridge.go
Normal 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
10
server/proto/basic.go
Normal 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
12
server/proto/handshake.go
Normal 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
24
server/proto/hosts.go
Normal 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
52
server/proto/net.go
Normal 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
12
server/proto/start.go
Normal 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
11
server/proto/time.go
Normal 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
72
server/router/router.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user