diff --git a/CMakeLists.txt b/CMakeLists.txt index 13d8c72..b8b7c18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,6 +148,10 @@ set(PROJECT_SOURCES src/ui/profile/edit_custom.cpp include/ui/profile/edit_custom.ui + include/ui/profile/edit_extra_core.h + src/ui/profile/edit_extra_core.cpp + include/ui/profile/edit_extra_core.ui + include/ui/profile/edit_wireguard.h src/ui/profile/edit_wireguard.cpp include/ui/profile/edit_wireguard.ui diff --git a/core/server/gen/libcore.pb.go b/core/server/gen/libcore.pb.go index 2b57aaa..b7ef267 100644 --- a/core/server/gen/libcore.pb.go +++ b/core/server/gen/libcore.pb.go @@ -138,11 +138,17 @@ func (x *ErrorResp) GetError() string { } type LoadConfigReq struct { - state protoimpl.MessageState `protogen:"open.v1"` - CoreConfig string `protobuf:"bytes,1,opt,name=core_config,json=coreConfig,proto3" json:"core_config,omitempty"` - DisableStats bool `protobuf:"varint,2,opt,name=disable_stats,json=disableStats,proto3" json:"disable_stats,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CoreConfig string `protobuf:"bytes,1,opt,name=core_config,json=coreConfig,proto3" json:"core_config,omitempty"` + DisableStats bool `protobuf:"varint,2,opt,name=disable_stats,json=disableStats,proto3" json:"disable_stats,omitempty"` + NeedExtraProcess bool `protobuf:"varint,3,opt,name=need_extra_process,json=needExtraProcess,proto3" json:"need_extra_process,omitempty"` + ExtraProcessPath string `protobuf:"bytes,4,opt,name=extra_process_path,json=extraProcessPath,proto3" json:"extra_process_path,omitempty"` + ExtraProcessArgs string `protobuf:"bytes,5,opt,name=extra_process_args,json=extraProcessArgs,proto3" json:"extra_process_args,omitempty"` + ExtraProcessConf string `protobuf:"bytes,6,opt,name=extra_process_conf,json=extraProcessConf,proto3" json:"extra_process_conf,omitempty"` + ExtraProcessConfDir string `protobuf:"bytes,7,opt,name=extra_process_conf_dir,json=extraProcessConfDir,proto3" json:"extra_process_conf_dir,omitempty"` + ExtraNoOut bool `protobuf:"varint,8,opt,name=extra_no_out,json=extraNoOut,proto3" json:"extra_no_out,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *LoadConfigReq) Reset() { @@ -189,6 +195,48 @@ func (x *LoadConfigReq) GetDisableStats() bool { return false } +func (x *LoadConfigReq) GetNeedExtraProcess() bool { + if x != nil { + return x.NeedExtraProcess + } + return false +} + +func (x *LoadConfigReq) GetExtraProcessPath() string { + if x != nil { + return x.ExtraProcessPath + } + return "" +} + +func (x *LoadConfigReq) GetExtraProcessArgs() string { + if x != nil { + return x.ExtraProcessArgs + } + return "" +} + +func (x *LoadConfigReq) GetExtraProcessConf() string { + if x != nil { + return x.ExtraProcessConf + } + return "" +} + +func (x *LoadConfigReq) GetExtraProcessConfDir() string { + if x != nil { + return x.ExtraProcessConfDir + } + return "" +} + +func (x *LoadConfigReq) GetExtraNoOut() bool { + if x != nil { + return x.ExtraNoOut + } + return false +} + type URLTestResp struct { state protoimpl.MessageState `protogen:"open.v1"` OutboundTag string `protobuf:"bytes,1,opt,name=outbound_tag,json=outboundTag,proto3" json:"outbound_tag,omitempty"` @@ -1193,12 +1241,29 @@ var file_libcore_proto_rawDesc = string([]byte{ 0x79, 0x52, 0x65, 0x71, 0x22, 0x0b, 0x0a, 0x09, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, 0x65, 0x73, 0x70, 0x22, 0x21, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, - 0x72, 0x72, 0x6f, 0x72, 0x22, 0x55, 0x0a, 0x0d, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x65, 0x71, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x72, 0x65, 0x5f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x72, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, - 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x64, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x22, 0x65, 0x0a, 0x0b, 0x55, + 0x72, 0x72, 0x6f, 0x72, 0x22, 0xe4, 0x02, 0x0a, 0x0d, 0x4c, 0x6f, 0x61, 0x64, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x72, 0x65, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x72, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x2c, 0x0a, 0x12, + 0x6e, 0x65, 0x65, 0x64, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6e, 0x65, 0x65, 0x64, 0x45, 0x78, + 0x74, 0x72, 0x61, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x65, 0x78, + 0x74, 0x72, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x70, 0x61, 0x74, 0x68, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x65, 0x78, 0x74, 0x72, 0x61, 0x50, 0x72, 0x6f, + 0x63, 0x65, 0x73, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2c, 0x0a, 0x12, 0x65, 0x78, 0x74, 0x72, + 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x65, 0x78, 0x74, 0x72, 0x61, 0x50, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x41, 0x72, 0x67, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, + 0x70, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x10, 0x65, 0x78, 0x74, 0x72, 0x61, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, + 0x43, 0x6f, 0x6e, 0x66, 0x12, 0x33, 0x0a, 0x16, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x70, 0x72, + 0x6f, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x65, 0x78, 0x74, 0x72, 0x61, 0x50, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x44, 0x69, 0x72, 0x12, 0x20, 0x0a, 0x0c, 0x65, 0x78, 0x74, + 0x72, 0x61, 0x5f, 0x6e, 0x6f, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4e, 0x6f, 0x4f, 0x75, 0x74, 0x22, 0x65, 0x0a, 0x0b, 0x55, 0x52, 0x4c, 0x54, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x61, 0x67, 0x12, 0x1d, 0x0a, diff --git a/core/server/gen/libcore.proto b/core/server/gen/libcore.proto index 9949c61..4f5cd91 100644 --- a/core/server/gen/libcore.proto +++ b/core/server/gen/libcore.proto @@ -38,6 +38,12 @@ message ErrorResp { message LoadConfigReq { string core_config = 1; bool disable_stats = 2; + bool need_extra_process = 3; + string extra_process_path = 4; + string extra_process_args = 5; + string extra_process_conf = 6; + string extra_process_conf_dir = 7; + bool extra_no_out = 8; } message URLTestResp { diff --git a/core/server/internal/process/logger.go b/core/server/internal/process/logger.go new file mode 100644 index 0000000..4e36aea --- /dev/null +++ b/core/server/internal/process/logger.go @@ -0,0 +1,15 @@ +package process + +import "log" + +type pipeLogger struct { + prefix string + noOut bool +} + +func (p *pipeLogger) Write(b []byte) (int, error) { + if !p.noOut { + log.Println(p.prefix + ":" + string(b)) + } + return len(b), nil +} diff --git a/core/server/internal/process/process.go b/core/server/internal/process/process.go new file mode 100644 index 0000000..31f8f8e --- /dev/null +++ b/core/server/internal/process/process.go @@ -0,0 +1,47 @@ +package process + +import ( + "fmt" + "github.com/sagernet/sing/common/atomic" + "os/exec" + "strings" +) + +type Process struct { + path string + args string + noOut bool + cmd *exec.Cmd + stopped atomic.Bool +} + +func NewProcess(path string, args string, noOut bool) *Process { + return &Process{path: path, args: args, noOut: noOut} +} + +func (p *Process) Start() error { + p.cmd = exec.Command(p.path, strings.Split(p.args, " ")...) + + p.cmd.Stdout = &pipeLogger{prefix: "Extra Core", noOut: p.noOut} + p.cmd.Stderr = &pipeLogger{prefix: "Extra Core", noOut: p.noOut} + + err := p.cmd.Start() + if err != nil { + return err + } + p.stopped.Store(false) + + go func() { + fmt.Println(p.path, ":", "process started, waiting for it to end") + _ = p.cmd.Wait() + if !p.stopped.Load() { + fmt.Println("Extra process exited unexpectedly") + } + }() + return nil +} + +func (p *Process) Stop() { + p.stopped.Store(true) + _ = p.cmd.Process.Kill() +} diff --git a/core/server/server.go b/core/server/server.go index b82d3e6..9010fe0 100644 --- a/core/server/server.go +++ b/core/server/server.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/settings" "github.com/sagernet/sing-box/experimental/clashapi" @@ -13,6 +14,7 @@ import ( "nekobox_core/gen" "nekobox_core/internal/boxbox" "nekobox_core/internal/boxmain" + "nekobox_core/internal/process" "nekobox_core/internal/sys" "os" "runtime" @@ -21,6 +23,7 @@ import ( ) var boxInstance *boxbox.Box +var extraProcess *process.Process var needUnsetDNS bool var systemProxyController settings.SystemProxy var systemProxyAddr metadata.Socksaddr @@ -59,6 +62,28 @@ func (s *server) Start(ctx context.Context, in *gen.LoadConfigReq) (out *gen.Err return } + if in.NeedExtraProcess { + extraConfPath := in.ExtraProcessConfDir + string(os.PathSeparator) + "extra.conf" + f, e := os.OpenFile(extraConfPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 700) + if e != nil { + err = E.Cause(e, "Failed to open extra.conf") + return + } + _, e = f.WriteString(in.ExtraProcessConf) + if e != nil { + err = E.Cause(e, "Failed to write extra.conf") + return + } + _ = f.Close() + args := fmt.Sprintf(in.ExtraProcessArgs, extraConfPath) + + extraProcess = process.NewProcess(in.ExtraProcessPath, args, in.ExtraNoOut) + err = extraProcess.Start() + if err != nil { + return + } + } + boxInstance, instanceCancel, err = boxmain.Create([]byte(in.CoreConfig)) if err != nil { return @@ -99,6 +124,11 @@ func (s *server) Stop(ctx context.Context, in *gen.EmptyReq) (out *gen.ErrorResp boxInstance = nil + if extraProcess != nil { + extraProcess.Stop() + extraProcess = nil + } + return } diff --git a/include/configs/ConfigBuilder.hpp b/include/configs/ConfigBuilder.hpp index a6a74d0..61690e4 100644 --- a/include/configs/ConfigBuilder.hpp +++ b/include/configs/ConfigBuilder.hpp @@ -4,10 +4,21 @@ #include "include/sys/Process.hpp" namespace NekoGui { + class ExtraCoreData + { + public: + QString path; + QString args; + QString config; + QString configDir; + bool noLog; + }; + class BuildConfigResult { public: QString error; QJsonObject coreConfig; + std::shared_ptr extraCoreData; QList> outboundStats; // all, but not including "bypass" "block" }; diff --git a/include/configs/proxy/ExtraCore.h b/include/configs/proxy/ExtraCore.h new file mode 100644 index 0000000..adc6cec --- /dev/null +++ b/include/configs/proxy/ExtraCore.h @@ -0,0 +1,34 @@ +#pragma once + +#include "AbstractBean.hpp" + +namespace NekoGui_fmt { + class ExtraCoreBean : public AbstractBean { + public: + QString socksAddress = "127.0.0.1"; + int socksPort; + QString extraCorePath; + QString extraCoreArgs; + QString extraCoreConf; + bool noLogs; + + ExtraCoreBean() : AbstractBean(0) { + _add(new configItem("socks_address", &socksAddress, itemType::string)); + _add(new configItem("socks_port", &socksPort, itemType::integer)); + _add(new configItem("extra_core_path", &extraCorePath, itemType::string)); + _add(new configItem("extra_core_args", &extraCoreArgs, itemType::string)); + _add(new configItem("extra_core_conf", &extraCoreConf, itemType::string)); + _add(new configItem("no_logs", &noLogs, itemType::boolean)); + }; + + QString DisplayType() override { return "ExtraCore"; }; + + CoreObjOutboundBuildResult BuildCoreObjSingBox() override; + + bool TryParseLink(const QString &link); + + bool TryParseJson(const QJsonObject &obj); + + QString ToShareLink() override; + }; +} diff --git a/include/configs/proxy/includes.h b/include/configs/proxy/includes.h index c957958..8219e34 100644 --- a/include/configs/proxy/includes.h +++ b/include/configs/proxy/includes.h @@ -9,3 +9,4 @@ #include "WireguardBean.h" #include "SSHBean.h" #include "CustomBean.hpp" +#include "ExtraCore.h" diff --git a/include/dataStore/Database.hpp b/include/dataStore/Database.hpp index 9d10a6f..220db2a 100644 --- a/include/dataStore/Database.hpp +++ b/include/dataStore/Database.hpp @@ -56,11 +56,16 @@ namespace NekoGui { void UpdateRouteChains(const QList>& newChain); + QStringList GetExtraCorePaths() const; + + bool AddExtraCorePath(const QString &path); + private: // sort by id QList profilesIdOrder; QList groupsIdOrder; QList routesIdOrder; + QSet extraCorePaths; [[nodiscard]] int NewProfileID() const; diff --git a/include/dataStore/ProxyEntity.hpp b/include/dataStore/ProxyEntity.hpp index c58df0a..2726439 100644 --- a/include/dataStore/ProxyEntity.hpp +++ b/include/dataStore/ProxyEntity.hpp @@ -3,6 +3,7 @@ #include "include/global/NekoGui.hpp" #include "include/stats/traffic/TrafficData.hpp" #include "include/configs/proxy/AbstractBean.hpp" +#include "include/configs/proxy/ExtraCore.h" namespace NekoGui_fmt { class SocksHttpBean; @@ -86,5 +87,9 @@ namespace NekoGui { [[nodiscard]] NekoGui_fmt::CustomBean *CustomBean() const { return (NekoGui_fmt::CustomBean *) bean.get(); }; + + [[nodiscard]] NekoGui_fmt::ExtraCoreBean *ExtraCoreBean() const { + return (NekoGui_fmt::ExtraCoreBean *) bean.get(); + }; }; } // namespace NekoGui diff --git a/include/ui/mainwindow.h b/include/ui/mainwindow.h index 5ebf6fa..eb13854 100644 --- a/include/ui/mainwindow.h +++ b/include/ui/mainwindow.h @@ -241,9 +241,9 @@ private: void url_test_current(); - void speedtest_current_group(const QList>& profiles); + void speedtest_current_group(const QList>& profiles, bool testCurrent = false); - void runSpeedTest(const QString& config, bool useDefault, const QStringList& outboundTags, const QMap& tag2entID, int entID = -1); + void runSpeedTest(const QString& config, bool useDefault, bool testCurrent, const QStringList& outboundTags, const QMap& tag2entID, int entID = -1); static void stop_core_daemon(); diff --git a/include/ui/profile/edit_extra_core.h b/include/ui/profile/edit_extra_core.h new file mode 100644 index 0000000..2330db1 --- /dev/null +++ b/include/ui/profile/edit_extra_core.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "profile_editor.h" +#include "ui_edit_extra_core.h" + +QT_BEGIN_NAMESPACE +namespace Ui { + class EditExtraCore; +} +QT_END_NAMESPACE + +class EditExtraCore : public QWidget, public ProfileEditor { + Q_OBJECT + +public: + explicit EditExtraCore(QWidget *parent = nullptr); + + ~EditExtraCore() override; + + void onStart(std::shared_ptr _ent) override; + + bool onEnd() override; + +private: + Ui::EditExtraCore *ui; + std::shared_ptr ent; +}; diff --git a/include/ui/profile/edit_extra_core.ui b/include/ui/profile/edit_extra_core.ui new file mode 100644 index 0000000..81a2af5 --- /dev/null +++ b/include/ui/profile/edit_extra_core.ui @@ -0,0 +1,155 @@ + + + EditExtraCore + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + Socks address + + + + + + + 127.0.0.1 + + + + + + + Socks port + + + + + + + 1080 + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + Core path + + + + + + + true + + + + + + + Choose from file + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + <html><head/><body><p>args to pass to the executable, with a %s placed in place of the config file path, for example:<br/>./sing-box run -c conf.json<br/>will have its args like:<br/>run -c %s</p></body></html> + + + Args + + + + + + + run -confPath %s + + + + + + + <html><head/><body><p>contents of the config file that will be passed to the extra core process</p></body></html> + + + Config + + + + + + + + + + No logs + + + + + + + + + + + + + + diff --git a/src/configs/ConfigBuilder.cpp b/src/configs/ConfigBuilder.cpp index f545dc6..dab17b8 100644 --- a/src/configs/ConfigBuilder.cpp +++ b/src/configs/ConfigBuilder.cpp @@ -46,6 +46,7 @@ namespace NekoGui { std::shared_ptr BuildConfig(const std::shared_ptr &ent, bool forTest, bool forExport, int chainID) { auto result = std::make_shared(); + result->extraCoreData = std::make_shared(); auto status = std::make_shared(); status->ent = ent; status->result = result; @@ -127,6 +128,11 @@ namespace NekoGui { QJsonArray directDomainArray; for (const auto &item: profiles) { + if (item->type == "extracore") + { + MW_show_log("Skipping ExtraCore conf"); + continue; + } if (!IsValid(item)) { MW_show_log("Skipping invalid config: " + item->bean->name); item->latency = -1; @@ -423,6 +429,15 @@ namespace NekoGui { // Outbounds auto tagProxy = BuildChain(status->chainID, status); if (!status->result->error.isEmpty()) return; + if (status->ent->type == "extracore") + { + auto bean = status->ent->ExtraCoreBean(); + status->result->extraCoreData->path = bean->extraCorePath; + status->result->extraCoreData->args = bean->extraCoreArgs; + status->result->extraCoreData->config = bean->extraCoreConf; + status->result->extraCoreData->configDir = GetBasePath(); + status->result->extraCoreData->noLog = bean->noLogs; + } // Direct domains bool needDirectDnsRules = false; diff --git a/src/configs/proxy/Bean2CoreObj_box.cpp b/src/configs/proxy/Bean2CoreObj_box.cpp index e9f13f6..36581f2 100644 --- a/src/configs/proxy/Bean2CoreObj_box.cpp +++ b/src/configs/proxy/Bean2CoreObj_box.cpp @@ -334,4 +334,17 @@ namespace NekoGui_fmt { return result; } + + CoreObjOutboundBuildResult ExtraCoreBean::BuildCoreObjSingBox() + { + CoreObjOutboundBuildResult result; + QJsonObject outbound{ + {"type", "socks"}, + {"server", socksAddress}, + {"server_port", socksPort}, + }; + result.outbound = outbound; + return result; + } + } // namespace NekoGui_fmt diff --git a/src/configs/proxy/Bean2Link.cpp b/src/configs/proxy/Bean2Link.cpp index a147645..e3ff95c 100644 --- a/src/configs/proxy/Bean2Link.cpp +++ b/src/configs/proxy/Bean2Link.cpp @@ -296,4 +296,9 @@ namespace NekoGui_fmt { return url.toString(QUrl::FullyEncoded); } + QString ExtraCoreBean::ToShareLink() + { + return "Unsupported for now"; + } + } // namespace NekoGui_fmt \ No newline at end of file diff --git a/src/configs/proxy/Json2Bean.cpp b/src/configs/proxy/Json2Bean.cpp index 30c1f85..7438695 100644 --- a/src/configs/proxy/Json2Bean.cpp +++ b/src/configs/proxy/Json2Bean.cpp @@ -7,6 +7,7 @@ #include #include +#include "include/configs/proxy/ExtraCore.h" #include "include/configs/proxy/SSHBean.h" namespace NekoGui_fmt @@ -224,4 +225,9 @@ namespace NekoGui_fmt return true; } + bool ExtraCoreBean::TryParseJson(const QJsonObject& obj) + { + return false; + } + } diff --git a/src/configs/proxy/Link2Bean.cpp b/src/configs/proxy/Link2Bean.cpp index 1c1d545..d524231 100644 --- a/src/configs/proxy/Link2Bean.cpp +++ b/src/configs/proxy/Link2Bean.cpp @@ -394,4 +394,10 @@ namespace NekoGui_fmt { return true; } + bool ExtraCoreBean::TryParseLink(const QString& link) + { + return false; + } + + } // namespace NekoGui_fmt \ No newline at end of file diff --git a/src/dataStore/Database.cpp b/src/dataStore/Database.cpp index 11661fd..19dec32 100644 --- a/src/dataStore/Database.cpp +++ b/src/dataStore/Database.cpp @@ -51,6 +51,7 @@ namespace NekoGui { continue; } profiles[id] = ent; + if (ent->type == "extracore") extraCorePaths.insert(ent->ExtraCoreBean()->extraCorePath); } // Clear Corrupted profile for (auto id: delProfile) { @@ -227,6 +228,8 @@ namespace NekoGui { bean = new NekoGui_fmt::SSHBean(NekoGui_fmt::SSHBean()); } else if (type == "custom") { bean = new NekoGui_fmt::CustomBean(); + } else if (type == "extracore") { + bean = new NekoGui_fmt::ExtraCoreBean(); } else { bean = new NekoGui_fmt::AbstractBean(-114514); } @@ -342,6 +345,19 @@ namespace NekoGui { return profiles.count(id) ? profiles[id] : nullptr; } + QStringList ProfileManager::GetExtraCorePaths() const { + return extraCorePaths.values(); + } + + bool ProfileManager::AddExtraCorePath(const QString &path) + { + if (extraCorePaths.contains(path)) + { + return false; + } + extraCorePaths.insert(path); + return true; + } // Group Group::Group() { diff --git a/src/sys/Process.cpp b/src/sys/Process.cpp index 2aac7bd..2cc6512 100644 --- a/src/sys/Process.cpp +++ b/src/sys/Process.cpp @@ -36,6 +36,11 @@ namespace NekoGui_sys { kill(); } } + if (log.contains("Extra process exited unexpectedly")) + { + MW_show_log("Extra Core exited, stopping profile..."); + MW_dialog_message("ExternalProcess", "Crashed"); + } if (logCounter.fetchAndAddRelaxed(log.count("\n")) > NekoGui::dataStore->max_log_line) return; MW_show_log(log); }); @@ -74,8 +79,8 @@ namespace NekoGui_sys { // Restart start_profile_when_core_is_up = NekoGui::dataStore->started_id; - MW_show_log("[ERROR] " + QObject::tr("Core exited, restarting.")); - setTimeout([=] { Restart(); }, this, 1000); + MW_show_log("[Fatal] " + QObject::tr("Core exited, restarting.")); + setTimeout([=] { Restart(); }, this, 200); } }); } diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 8685070..83bff45 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -454,7 +454,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi { if (running != nullptr) { - speedtest_current_group({running}); + speedtest_current_group({}, true); } }); connect(ui->actionSpeedtest_Selected, &QAction::triggered, this, [=]() diff --git a/src/ui/mainwindow_grpc.cpp b/src/ui/mainwindow_grpc.cpp index fe71f4b..5db2cec 100644 --- a/src/ui/mainwindow_grpc.cpp +++ b/src/ui/mainwindow_grpc.cpp @@ -170,9 +170,9 @@ void MainWindow::url_test_current() { }); } -void MainWindow::speedtest_current_group(const QList>& profiles) +void MainWindow::speedtest_current_group(const QList>& profiles, bool testCurrent) { - if (profiles.isEmpty()) { + if (profiles.isEmpty() && !testCurrent) { return; } if (!speedtestRunning.tryLock()) { @@ -180,22 +180,29 @@ void MainWindow::speedtest_current_group(const QListerror.isEmpty()) { - MW_show_log(tr("Failed to build test config: ") + buildObject->error); - speedtestRunning.unlock(); - return; - } + runOnNewThread([this, profiles, testCurrent]() { + if (!testCurrent) + { + auto buildObject = NekoGui::BuildTestConfig(profiles); + if (!buildObject->error.isEmpty()) { + MW_show_log(tr("Failed to build test config: ") + buildObject->error); + speedtestRunning.unlock(); + return; + } - stopSpeedtest.store(false); - for (const auto &entID: buildObject->fullConfigs.keys()) { - auto configStr = buildObject->fullConfigs[entID]; - runSpeedTest(configStr, true, {}, {}, entID); - } + stopSpeedtest.store(false); + for (const auto &entID: buildObject->fullConfigs.keys()) { + auto configStr = buildObject->fullConfigs[entID]; + runSpeedTest(configStr, true, false, {}, {}, entID); + } - if (!buildObject->outboundTags.empty()) { - runSpeedTest(QJsonObject2QString(buildObject->coreConfig, false), false, buildObject->outboundTags, buildObject->tag2entID); + if (!buildObject->outboundTags.empty()) { + runSpeedTest(QJsonObject2QString(buildObject->coreConfig, false), false, false, buildObject->outboundTags, buildObject->tag2entID); + } + } else + { + stopSpeedtest.store(false); + runSpeedTest("", true, true, {}, {}); } speedtestRunning.unlock(); @@ -206,7 +213,7 @@ void MainWindow::speedtest_current_group(const QList& tag2entID, int entID) +void MainWindow::runSpeedTest(const QString& config, bool useDefault, bool testCurrent, const QStringList& outboundTags, const QMap& tag2entID, int entID) { if (stopSpeedtest.load()) { MW_show_log(tr("Profile speed test aborted")); @@ -222,6 +229,7 @@ void MainWindow::runSpeedTest(const QString& config, bool useDefault, const QStr req.set_use_default_outbound(useDefault); req.set_test_download(speedtestConf == NekoGui::TestConfig::FULL || speedtestConf == NekoGui::TestConfig::DL); req.set_test_upload(speedtestConf == NekoGui::TestConfig::FULL || speedtestConf == NekoGui::TestConfig::UL); + req.set_test_current(testCurrent); // loop query result auto doneMu = new QMutex; @@ -240,7 +248,7 @@ void MainWindow::runSpeedTest(const QString& config, bool useDefault, const QStr { continue; } - auto profile = NekoGui::profileManager->GetProfile(tag2entID[res.result().outbound_tag().c_str()]); + auto profile = testCurrent ? running : NekoGui::profileManager->GetProfile(tag2entID[res.result().outbound_tag().c_str()]); if (profile == nullptr) { continue; @@ -268,7 +276,8 @@ void MainWindow::runSpeedTest(const QString& config, bool useDefault, const QStr if (!rpcOK) return; for (const auto &res: result.results()) { - if (!tag2entID.empty()) { + if (testCurrent) entID = running ? running->id : -1; + else { entID = tag2entID.count(QString(res.outbound_tag().c_str())) == 0 ? -1 : tag2entID[QString(res.outbound_tag().c_str())]; } if (entID == -1) { @@ -361,6 +370,15 @@ void MainWindow::neko_start(int _id) { libcore::LoadConfigReq req; req.set_core_config(QJsonObject2QString(result->coreConfig, true).toStdString()); req.set_disable_stats(NekoGui::dataStore->disable_traffic_stats); + if (ent->type == "extracore") + { + req.set_need_extra_process(true); + req.set_extra_process_path(result->extraCoreData->path.toStdString()); + req.set_extra_process_args(result->extraCoreData->args.toStdString()); + req.set_extra_process_conf(result->extraCoreData->config.toStdString()); + req.set_extra_process_conf_dir(result->extraCoreData->configDir.toStdString()); + req.set_extra_no_out(result->extraCoreData->noLog); + } // bool rpcOK; QString error = defaultClient->Start(&rpcOK, req); diff --git a/src/ui/profile/dialog_edit_profile.cpp b/src/ui/profile/dialog_edit_profile.cpp index 83c2f62..6bf8aa3 100644 --- a/src/ui/profile/dialog_edit_profile.cpp +++ b/src/ui/profile/dialog_edit_profile.cpp @@ -9,6 +9,7 @@ #include "include/ui/profile/edit_wireguard.h" #include "include/ui/profile/edit_ssh.h" #include "include/ui/profile/edit_custom.h" +#include "include/ui/profile/edit_extra_core.h" #include "include/configs/proxy/includes.h" #include "include/configs/proxy/Preset.hpp" @@ -162,6 +163,7 @@ DialogEditProfile::DialogEditProfile(const QString &_type, int profileOrGroupId, LOAD_TYPE("ssh") ui->type->addItem(tr("Custom (%1 outbound)").arg(software_core_name), "internal"); ui->type->addItem(tr("Custom (%1 config)").arg(software_core_name), "internal-full"); + ui->type->addItem(tr("Extra Core"), "extracore"); LOAD_TYPE("chain") // type changed @@ -229,6 +231,13 @@ void DialogEditProfile::typeSelected(const QString &newType) { customType = newEnt ? type : ent->CustomBean()->core; if (customType != "custom") _innerWidget->preset_core = customType; type = "custom"; + ui->apply_to_group->hide(); + } else if (type == "extracore") + { + auto _innerWidget = new EditExtraCore(this); + innerWidget = _innerWidget; + innerEditor = _innerWidget; + ui->apply_to_group->hide(); } else { validType = false; } @@ -244,7 +253,7 @@ void DialogEditProfile::typeSelected(const QString &newType) { } // hide some widget - auto showAddressPort = type != "chain" && customType != "internal" && customType != "internal-full"; + auto showAddressPort = type != "chain" && customType != "internal" && customType != "internal-full" && type != "extracore"; ui->address->setVisible(showAddressPort); ui->address_l->setVisible(showAddressPort); ui->port->setVisible(showAddressPort); @@ -296,6 +305,10 @@ void DialogEditProfile::typeSelected(const QString &newType) { show_custom_outbound = false; show_custom_config = false; } + } else if (type == "extracore") + { + show_custom_outbound = false; + show_custom_config = false; } ui->custom_box->setVisible(show_custom_outbound); ui->custom_global_box->setVisible(show_custom_config); @@ -309,7 +322,7 @@ void DialogEditProfile::typeSelected(const QString &newType) { delete old; // 左边 bean inner editor - innerEditor->get_edit_dialog = [&]() { return (QWidget *) this; }; + innerEditor->get_edit_dialog = [&]() { return static_cast(this); }; innerEditor->get_edit_text_name = [&]() { return ui->name->text(); }; innerEditor->get_edit_text_serverAddress = [&]() { return ui->address->text(); }; innerEditor->get_edit_text_serverPort = [&]() { return ui->port->text(); }; diff --git a/src/ui/profile/edit_chain.cpp b/src/ui/profile/edit_chain.cpp index 70d33bb..9094432 100644 --- a/src/ui/profile/edit_chain.cpp +++ b/src/ui/profile/edit_chain.cpp @@ -50,7 +50,7 @@ void EditChain::on_select_profile_clicked() { void EditChain::AddProfileToListIfExist(int profileId) { auto _ent = NekoGui::profileManager->GetProfile(profileId); - if (_ent != nullptr && _ent->type != "chain") { + if (_ent != nullptr && _ent->type != "chain" && _ent->type != "extracore") { auto wI = new QListWidgetItem(); wI->setData(114514, profileId); auto w = new ProxyItem(this, _ent, wI); @@ -69,7 +69,7 @@ void EditChain::AddProfileToListIfExist(int profileId) { void EditChain::ReplaceProfile(ProxyItem *w, int profileId) { auto _ent = NekoGui::profileManager->GetProfile(profileId); - if (_ent != nullptr && _ent->type != "chain") { + if (_ent != nullptr && _ent->type != "chain" && _ent->type != "extracore") { w->item->setData(114514, profileId); w->ent = _ent; w->refresh_data(); diff --git a/src/ui/profile/edit_extra_core.cpp b/src/ui/profile/edit_extra_core.cpp new file mode 100644 index 0000000..3f1766a --- /dev/null +++ b/src/ui/profile/edit_extra_core.cpp @@ -0,0 +1,61 @@ +#include "include/ui/profile/edit_extra_core.h" + +#include + +#include "include/configs/proxy/ExtraCore.h" +#include "include/configs/proxy/Preset.hpp" +#include "include/ui/profile/dialog_edit_profile.h" + +EditExtraCore::EditExtraCore(QWidget *parent) : QWidget(parent), + ui(new Ui::EditExtraCore) { + ui->setupUi(this); +} + +EditExtraCore::~EditExtraCore() { + delete ui; +} + +void EditExtraCore::onStart(std::shared_ptr _ent) { + this->ent = _ent; + + auto bean = ent->ExtraCoreBean(); + ui->socks_address->setText(bean->socksAddress); + ui->socks_port->setValidator(new QIntValidator(1, 65534)); + ui->socks_port->setText(Int2String(bean->socksPort)); + ui->config->setPlainText(bean->extraCoreConf); + ui->args->setText(bean->extraCoreArgs); + ui->path_combo->addItems(NekoGui::profileManager->GetExtraCorePaths()); + ui->path_combo->setCurrentText(bean->extraCorePath); + + connect(ui->path_button, &QPushButton::pressed, this, [=] + { + auto f = QFileDialog::getOpenFileName(); + if (f.isEmpty()) + { + return; + } + if (NekoGui::profileManager->AddExtraCorePath(f)) ui->path_combo->addItem(f); + ui->path_combo->setCurrentText(f); + ui->path_combo->setSizeAdjustPolicy(QComboBox::AdjustToContents); + adjustSize(); + }); +} + +bool EditExtraCore::onEnd() { + auto bean = ent->ExtraCoreBean(); + if (!ui->args->text().contains("%s")) + { + runOnUiThread([=] + { + MessageBoxWarning("Invalid args", "Args should have a %s as the placeholder for config file path."); + }); + return false; + } + bean->socksAddress = ui->socks_address->text(); + bean->socksPort = ui->socks_port->text().toInt(); + bean->extraCoreConf = ui->config->toPlainText(); + bean->extraCorePath = ui->path_combo->currentText(); + bean->extraCoreArgs = ui->args->text(); + + return true; +}