add extra core &&

fix speedtest current
This commit is contained in:
Nova 2025-05-15 11:56:08 +03:30
parent 83413c19e8
commit 78a78cf38e
26 changed files with 603 additions and 39 deletions

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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> extraCoreData;
QList<std::shared_ptr<NekoGui_traffic::TrafficData>> outboundStats; // all, but not including "bypass" "block"
};

View File

@ -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;
};
}

View File

@ -9,3 +9,4 @@
#include "WireguardBean.h"
#include "SSHBean.h"
#include "CustomBean.hpp"
#include "ExtraCore.h"

View File

@ -56,11 +56,16 @@ namespace NekoGui {
void UpdateRouteChains(const QList<std::shared_ptr<RoutingChain>>& newChain);
QStringList GetExtraCorePaths() const;
bool AddExtraCorePath(const QString &path);
private:
// sort by id
QList<int> profilesIdOrder;
QList<int> groupsIdOrder;
QList<int> routesIdOrder;
QSet<QString> extraCorePaths;
[[nodiscard]] int NewProfileID() const;

View File

@ -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

View File

@ -241,9 +241,9 @@ private:
void url_test_current();
void speedtest_current_group(const QList<std::shared_ptr<NekoGui::ProxyEntity>>& profiles);
void speedtest_current_group(const QList<std::shared_ptr<NekoGui::ProxyEntity>>& profiles, bool testCurrent = false);
void runSpeedTest(const QString& config, bool useDefault, const QStringList& outboundTags, const QMap<QString, int>& tag2entID, int entID = -1);
void runSpeedTest(const QString& config, bool useDefault, bool testCurrent, const QStringList& outboundTags, const QMap<QString, int>& tag2entID, int entID = -1);
static void stop_core_daemon();

View File

@ -0,0 +1,28 @@
#pragma once
#include <QWidget>
#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<NekoGui::ProxyEntity> _ent) override;
bool onEnd() override;
private:
Ui::EditExtraCore *ui;
std::shared_ptr<NekoGui::ProxyEntity> ent;
};

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditExtraCore</class>
<widget class="QWidget" name="EditExtraCore">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Socks address</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="socks_address">
<property name="text">
<string>127.0.0.1</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Socks port</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="socks_port">
<property name="text">
<string>1080</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Core path</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="path_combo">
<property name="frame">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="path_button">
<property name="text">
<string>Choose from file</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_4">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;args to pass to the executable, with a %s placed in place of the config file path, for example:&lt;br/&gt;./sing-box run -c conf.json&lt;br/&gt;will have its args like:&lt;br/&gt;run -c %s&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Args</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="args">
<property name="placeholderText">
<string>run -confPath %s</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;contents of the config file that will be passed to the extra core process&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Config</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPlainTextEdit" name="config"/>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="no_logs">
<property name="text">
<string>No logs</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -46,6 +46,7 @@ namespace NekoGui {
std::shared_ptr<BuildConfigResult> BuildConfig(const std::shared_ptr<ProxyEntity> &ent, bool forTest, bool forExport, int chainID) {
auto result = std::make_shared<BuildConfigResult>();
result->extraCoreData = std::make_shared<ExtraCoreData>();
auto status = std::make_shared<BuildConfigStatus>();
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;

View File

@ -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

View File

@ -296,4 +296,9 @@ namespace NekoGui_fmt {
return url.toString(QUrl::FullyEncoded);
}
QString ExtraCoreBean::ToShareLink()
{
return "Unsupported for now";
}
} // namespace NekoGui_fmt

View File

@ -7,6 +7,7 @@
#include <include/configs/proxy/VMessBean.hpp>
#include <include/configs/proxy/WireguardBean.h>
#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;
}
}

View File

@ -394,4 +394,10 @@ namespace NekoGui_fmt {
return true;
}
bool ExtraCoreBean::TryParseLink(const QString& link)
{
return false;
}
} // namespace NekoGui_fmt

View File

@ -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() {

View File

@ -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);
}
});
}

View File

@ -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, [=]()

View File

@ -170,9 +170,9 @@ void MainWindow::url_test_current() {
});
}
void MainWindow::speedtest_current_group(const QList<std::shared_ptr<NekoGui::ProxyEntity>>& profiles)
void MainWindow::speedtest_current_group(const QList<std::shared_ptr<NekoGui::ProxyEntity>>& profiles, bool testCurrent)
{
if (profiles.isEmpty()) {
if (profiles.isEmpty() && !testCurrent) {
return;
}
if (!speedtestRunning.tryLock()) {
@ -180,22 +180,29 @@ void MainWindow::speedtest_current_group(const QList<std::shared_ptr<NekoGui::Pr
return;
}
runOnNewThread([this, profiles]() {
auto buildObject = NekoGui::BuildTestConfig(profiles);
if (!buildObject->error.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<std::shared_ptr<NekoGui::Pr
});
}
void MainWindow::runSpeedTest(const QString& config, bool useDefault, const QStringList& outboundTags, const QMap<QString, int>& tag2entID, int entID)
void MainWindow::runSpeedTest(const QString& config, bool useDefault, bool testCurrent, const QStringList& outboundTags, const QMap<QString, int>& 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);

View File

@ -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<QWidget*>(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(); };

View File

@ -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();

View File

@ -0,0 +1,61 @@
#include "include/ui/profile/edit_extra_core.h"
#include <QFileDialog>
#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<NekoGui::ProxyEntity> _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;
}