new: add multiprocess safe database
This commit is contained in:
6
.github/change_version.sh
vendored
6
.github/change_version.sh
vendored
@@ -5,14 +5,14 @@ SED() { [[ "$OSTYPE" == "darwin"* ]] && sed -i '' "$@" || sed -i "$@"; }
|
||||
echo "previous version was $(git describe --tags $(git rev-list --tags --max-count=1))"
|
||||
echo "WARNING: This operation will creates version tag and push to github"
|
||||
read -p "Version? (provide the next x.y.z semver) : " TAG
|
||||
echo $TAG &&\
|
||||
echo $TAG
|
||||
[[ "$TAG" =~ ^[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2}(\.dev)?$ ]] || { echo "Incorrect tag. e.g., 1.2.3 or 1.2.3.dev"; exit 1; }
|
||||
IFS="." read -r -a VERSION_ARRAY <<< "$TAG"
|
||||
VERSION_STR="${VERSION_ARRAY[0]}.${VERSION_ARRAY[1]}.${VERSION_ARRAY[2]}"
|
||||
BUILD_NUMBER=$(( ${VERSION_ARRAY[0]} * 10000 + ${VERSION_ARRAY[1]} * 100 + ${VERSION_ARRAY[2]} ))
|
||||
echo "version: ${VERSION_STR}+${BUILD_NUMBER}"
|
||||
SED -e "s|<key>CFBundleVersion</key>\s*<string>[^<]*</string>|<key>CFBundleVersion</key><string>${VERSION_STR}</string>|" Info.plist &&\
|
||||
SED -e "s|<key>CFBundleShortVersionString</key>\s*<string>[^<]*</string>|<key>CFBundleShortVersionString</key><string>${VERSION_STR}</string>|" Info.plist &&\
|
||||
SED -e "s|<key>CFBundleVersion</key>\s*<string>[^<]*</string>|<key>CFBundleVersion</key><string>${VERSION_STR}</string>|" Info.plist
|
||||
SED -e "s|<key>CFBundleShortVersionString</key>\s*<string>[^<]*</string>|<key>CFBundleShortVersionString</key><string>${VERSION_STR}</string>|" Info.plist
|
||||
SED "s|ENV VERSION=.*|ENV VERSION=v${TAG}|g" docker/Dockerfile
|
||||
git add Info.plist docker/Dockerfile
|
||||
git commit -m "release: version ${TAG}"
|
||||
|
||||
@@ -80,6 +80,7 @@ type MuxOptions struct {
|
||||
}
|
||||
|
||||
type WarpOptions struct {
|
||||
UniqueId string `json:"unique-id"`
|
||||
EnableWarp bool `json:"enable"`
|
||||
Mode string `json:"mode"`
|
||||
WireguardConfigStr string `json:"wireguard-config"`
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
// "github.com/bepass-org/wireguard-go/warp"
|
||||
"github.com/hiddify/hiddify-core/v2/db"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
T "github.com/sagernet/sing-box/option"
|
||||
@@ -143,34 +144,31 @@ func GenerateWarpInfo(license string, oldAccountId string, oldAccessToken string
|
||||
return &identity, res, &warpcfg, err
|
||||
}
|
||||
|
||||
func getOrGenerateWarpLocallyIfNeeded(key string, warpOptions *WarpOptions) WarpWireguardConfig {
|
||||
if warpOptions == nil {
|
||||
warpOptions = &WarpOptions{}
|
||||
}
|
||||
func getOrGenerateWarpLocallyIfNeeded(warpOptions *WarpOptions) WarpWireguardConfig {
|
||||
if warpOptions.WireguardConfig.PrivateKey != "" {
|
||||
return warpOptions.WireguardConfig
|
||||
}
|
||||
common.Storage.GetExtensionData("hiddify.warp."+key, &warpOptions)
|
||||
if warpOptions.WireguardConfig.PrivateKey != "" {
|
||||
table := db.GetTable[WarpOptions]()
|
||||
dbWarpOptions, err := table.First(func(data WarpOptions) bool { return data.UniqueId == warpOptions.UniqueId })
|
||||
if err == nil && dbWarpOptions.WireguardConfig.PrivateKey != "" {
|
||||
return warpOptions.WireguardConfig
|
||||
}
|
||||
license := ""
|
||||
if len(key) > 28 && key[2] == '_' { // warp key is 26 characters long
|
||||
license = key[3:]
|
||||
if len(warpOptions.UniqueId) > 28 && warpOptions.UniqueId[2] == '_' { // warp key is 26 characters long
|
||||
license = warpOptions.UniqueId[3:]
|
||||
}
|
||||
accountidentity, _, wireguardConfig, err := GenerateWarpInfo(license, warpOptions.Account.AccountID, warpOptions.Account.AccessToken)
|
||||
if err != nil {
|
||||
return WarpWireguardConfig{}
|
||||
}
|
||||
newoption := WarpOptions{
|
||||
WireguardConfig: *wireguardConfig,
|
||||
Account: WarpAccount{
|
||||
AccountID: accountidentity.ID,
|
||||
AccessToken: accountidentity.Token,
|
||||
},
|
||||
warpOptions.Account = WarpAccount{
|
||||
AccountID: accountidentity.ID,
|
||||
AccessToken: accountidentity.Token,
|
||||
}
|
||||
common.Storage.SaveExtensionData("hiddify.warp."+key, &newoption)
|
||||
return newoption.WireguardConfig
|
||||
warpOptions.WireguardConfig = *wireguardConfig
|
||||
table.ReplaceOrInsert(func(data WarpOptions) bool { return data.UniqueId == warpOptions.UniqueId }, *warpOptions)
|
||||
|
||||
return *wireguardConfig
|
||||
}
|
||||
|
||||
func patchWarp(base *option.Outbound, configOpt *HiddifyOptions, final bool, staticIpsDns map[string][]string) error {
|
||||
@@ -199,8 +197,13 @@ func patchWarp(base *option.Outbound, configOpt *HiddifyOptions, final bool, sta
|
||||
warpOpt = &configOpt.Warp
|
||||
} else if key == "p2" {
|
||||
warpOpt = &configOpt.Warp2
|
||||
} else {
|
||||
warpOpt = &WarpOptions{
|
||||
UniqueId: key,
|
||||
}
|
||||
}
|
||||
wireguardConfig = getOrGenerateWarpLocallyIfNeeded(key, warpOpt)
|
||||
warpOpt.UniqueId = key
|
||||
wireguardConfig = getOrGenerateWarpLocallyIfNeeded(warpOpt)
|
||||
} else {
|
||||
_, _, wgConfig, err := GenerateWarpInfo(key, "", "")
|
||||
if err != nil {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/hiddify/hiddify-core/config"
|
||||
"github.com/hiddify/hiddify-core/extension/ui"
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"github.com/hiddify/hiddify-core/v2/common"
|
||||
"github.com/hiddify/hiddify-core/v2/db"
|
||||
"github.com/jellydator/validation"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -12,7 +14,7 @@ import (
|
||||
|
||||
type Extension interface {
|
||||
GetUI() ui.Form
|
||||
SubmitData(data map[string]string) error
|
||||
SubmitData(button string, data map[string]string) error
|
||||
Cancel() error
|
||||
Stop() error
|
||||
UpdateUI(form ui.Form) error
|
||||
@@ -41,13 +43,37 @@ func (b *Base[T]) BeforeAppConnect(hiddifySettings *config.HiddifyOptions, singc
|
||||
}
|
||||
|
||||
func (b *Base[T]) StoreData() {
|
||||
common.Storage.SaveExtensionData(b.id, &b.Data)
|
||||
table := db.GetTable[extensionData]()
|
||||
table.Update(func(s extensionData) extensionData {
|
||||
s.Data = b.Data
|
||||
return s
|
||||
}, func(data extensionData) bool {
|
||||
return data.Id == b.getId()
|
||||
})
|
||||
}
|
||||
|
||||
func (b *Base[T]) init(id string) {
|
||||
b.id = id
|
||||
b.queue = make(chan *pb.ExtensionResponse, 1)
|
||||
common.Storage.GetExtensionData(b.id, &b.Data)
|
||||
table := db.GetTable[extensionData]()
|
||||
extdata, err := table.First(func(data extensionData) bool { return data.Id == b.id })
|
||||
if err != nil {
|
||||
log.Warn("error: ", err)
|
||||
return
|
||||
}
|
||||
if extdata == nil {
|
||||
log.Warn("extension data not found ", id)
|
||||
return
|
||||
}
|
||||
if extdata.Data != nil {
|
||||
if data, ok := extdata.Data.(*T); ok {
|
||||
b.Data = *data
|
||||
} else {
|
||||
var t T
|
||||
name := reflect.TypeOf(t).Name()
|
||||
log.Warn("current extension data of ,", id, " is not of type ", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Base[T]) getQueue() chan *pb.ExtensionResponse {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"log"
|
||||
|
||||
pb "github.com/hiddify/hiddify-core/hiddifyrpc"
|
||||
"github.com/hiddify/hiddify-core/v2/common"
|
||||
"github.com/hiddify/hiddify-core/v2/db"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
@@ -18,141 +18,71 @@ func (ExtensionHostService) ListExtensions(ctx context.Context, empty *pb.Empty)
|
||||
extensionList := &pb.ExtensionList{
|
||||
Extensions: make([]*pb.Extension, 0),
|
||||
}
|
||||
|
||||
for _, extension := range allExtensionsMap {
|
||||
allext, err := db.GetTable[extensionData]().All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, dbext := range allext {
|
||||
ext := allExtensionsMap[dbext.Id]
|
||||
extensionList.Extensions = append(extensionList.Extensions, &pb.Extension{
|
||||
Id: extension.Id,
|
||||
Title: extension.Title,
|
||||
Description: extension.Description,
|
||||
Enable: generalExtensionData.ExtensionStatusMap[extension.Id],
|
||||
Id: ext.Id,
|
||||
Title: ext.Title,
|
||||
Description: ext.Description,
|
||||
Enable: dbext.Enable,
|
||||
})
|
||||
}
|
||||
|
||||
return extensionList, nil
|
||||
}
|
||||
|
||||
func getExtension(id string) (*Extension, error) {
|
||||
if !isEnable(id) {
|
||||
return nil, fmt.Errorf("Extension with ID %s is not enabled", id)
|
||||
}
|
||||
if extension, ok := enabledExtensionsMap[id]; ok {
|
||||
return extension, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Extension with ID %s not found", id)
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) Connect(req *pb.ExtensionRequest, stream grpc.ServerStreamingServer[pb.ExtensionResponse]) error {
|
||||
// Get the extension from the map using the Extension ID
|
||||
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
|
||||
extension, err := getExtension(req.GetExtensionId())
|
||||
if err != nil {
|
||||
log.Printf("Error connecting stream for extension %s: %v", req.GetExtensionId(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Connecting stream for extension %s", req.GetExtensionId())
|
||||
log.Printf("Extension data: %+v", extension)
|
||||
// Handle loading the UI for the extension
|
||||
// Call extension-specific logic to generate UI data
|
||||
// if err := platform.connect(stream); err != nil {
|
||||
// log.Printf("Error connecting stream for extension %s: %v", req.GetExtensionId(), err)
|
||||
// }
|
||||
if err := (*extension).UpdateUI((*extension).GetUI()); err != nil {
|
||||
log.Printf("Error updating UI for extension %s: %v", req.GetExtensionId(), err)
|
||||
}
|
||||
// info := <-platform.queue
|
||||
log.Printf("Connecting stream for extension %s", req.GetExtensionId())
|
||||
log.Printf("Extension data: %+v", extension)
|
||||
|
||||
// stream.Send(info)
|
||||
// (*platform.extension).SubmitData(map[string]string{})
|
||||
// log.Printf("Extension info: %+v", info)
|
||||
// // Handle submitting data to the extension
|
||||
// case pb.ExtensionRequestType_SUBMIT_DATA:
|
||||
// // Handle submitting data to the extension
|
||||
// // Process the provided data
|
||||
// err := extension.SubmitData(req.GetData())
|
||||
// if err != nil {
|
||||
// log.Printf("Error submitting data for extension %s: %v", req.GetExtensionId(), err)
|
||||
// // continue
|
||||
// }
|
||||
if err := (*extension).UpdateUI((*extension).GetUI()); err != nil {
|
||||
log.Printf("Error updating UI for extension %s: %v", req.GetExtensionId(), err)
|
||||
}
|
||||
|
||||
// case hiddifyrpc.ExtensionRequestType_CANCEL:
|
||||
// // Handle canceling the current operation in the extension
|
||||
// extension.Stop()
|
||||
// log.Printf("Operation canceled for extension %s", req.GetExtensionId())
|
||||
|
||||
// default:
|
||||
// log.Printf("Unknown request type: %v", req.GetType())
|
||||
// }
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case info := <-(*extension).getQueue():
|
||||
stream.Send(info)
|
||||
if info.GetType() == pb.ExtensionResponseType_END {
|
||||
return nil
|
||||
case info := <-(*extension).getQueue():
|
||||
stream.Send(info)
|
||||
if info.GetType() == pb.ExtensionResponseType_END {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// break
|
||||
// case <-stopCh:
|
||||
// break
|
||||
// // case info := <-sub:
|
||||
// // stream.Send(&info)
|
||||
// case <-time.After(1000 * time.Millisecond):
|
||||
// }
|
||||
|
||||
// extension := extensionsMap[data.GetExtensionId()]
|
||||
// ui := extension.GetUI(data.Data)
|
||||
|
||||
// return &pb.UI{
|
||||
// ExtensionId: data.GetExtensionId(),
|
||||
// JsonUi: ui.ToJSON(),
|
||||
// }, nil
|
||||
} else {
|
||||
log.Printf("Extension with ID %s not found", req.GetExtensionId())
|
||||
return fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
|
||||
}
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) SubmitForm(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
|
||||
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
|
||||
(*extension).SubmitData(req.GetData())
|
||||
|
||||
extension, err := getExtension(req.GetExtensionId())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_OK,
|
||||
Message: "Success",
|
||||
}, nil
|
||||
Code: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) Cancel(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
|
||||
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
|
||||
(*extension).Cancel()
|
||||
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_OK,
|
||||
Message: "Success",
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) Stop(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
|
||||
if extension, ok := enabledExtensionsMap[req.GetExtensionId()]; ok {
|
||||
(*extension).Stop()
|
||||
(*extension).StoreData()
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_OK,
|
||||
Message: "Success",
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Extension with ID %s not found", req.GetExtensionId())
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) EditExtension(ctx context.Context, req *pb.EditExtensionRequest) (*pb.ExtensionActionResult, error) {
|
||||
generalExtensionData.ExtensionStatusMap[req.GetExtensionId()] = req.Enable
|
||||
if !req.Enable {
|
||||
ext := *enabledExtensionsMap[req.GetExtensionId()]
|
||||
if ext != nil {
|
||||
ext.Stop()
|
||||
ext.StoreData()
|
||||
}
|
||||
delete(enabledExtensionsMap, req.GetExtensionId())
|
||||
} else {
|
||||
loadExtension(allExtensionsMap[req.GetExtensionId()])
|
||||
}
|
||||
common.Storage.SaveExtensionData("default", generalExtensionData)
|
||||
(*extension).SubmitData(req.GetData())
|
||||
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
@@ -160,3 +90,75 @@ func (e ExtensionHostService) EditExtension(ctx context.Context, req *pb.EditExt
|
||||
Message: "Success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) Cancel(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
|
||||
extension, err := getExtension(req.GetExtensionId())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
(*extension).Cancel()
|
||||
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_OK,
|
||||
Message: "Success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) Stop(ctx context.Context, req *pb.ExtensionRequest) (*pb.ExtensionActionResult, error) {
|
||||
extension, err := getExtension(req.GetExtensionId())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_FAILED,
|
||||
Message: err.Error(),
|
||||
}, err
|
||||
}
|
||||
(*extension).Stop()
|
||||
(*extension).StoreData()
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_OK,
|
||||
Message: "Success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e ExtensionHostService) EditExtension(ctx context.Context, req *pb.EditExtensionRequest) (*pb.ExtensionActionResult, error) {
|
||||
if !req.Enable {
|
||||
extension, _ := getExtension(req.GetExtensionId())
|
||||
if extension != nil {
|
||||
(*extension).Stop()
|
||||
(*extension).StoreData()
|
||||
}
|
||||
delete(enabledExtensionsMap, req.GetExtensionId())
|
||||
}
|
||||
table := db.GetTable[extensionData]()
|
||||
table.Update(func(s extensionData) extensionData {
|
||||
s.Enable = req.Enable
|
||||
return s
|
||||
}, func(data extensionData) bool {
|
||||
return data.Id == req.GetExtensionId()
|
||||
})
|
||||
|
||||
if req.Enable {
|
||||
loadExtension(allExtensionsMap[req.GetExtensionId()])
|
||||
}
|
||||
|
||||
return &pb.ExtensionActionResult{
|
||||
ExtensionId: req.ExtensionId,
|
||||
Code: pb.ResponseCode_OK,
|
||||
Message: "Success",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type extensionData struct {
|
||||
Id string `json:"id"`
|
||||
Enable bool `json:"enable"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
@@ -708,7 +708,7 @@ function connect() {
|
||||
if(response.getType()== proto.hiddifyrpc.ExtensionResponseType.SHOW_DIALOG) {
|
||||
renderForm(ui, "dialog",handleSubmitButtonClick,handleCancelButtonClick,undefined);
|
||||
}else{
|
||||
renderForm(ui, "",handleSubmitButtonClick,handleCancelButtonClick);
|
||||
renderForm(ui, "",handleSubmitButtonClick,handleCancelButtonClick,handleStopButtonClick);
|
||||
}
|
||||
|
||||
|
||||
@@ -2576,11 +2576,16 @@ function renderForm(json, dialog, submitAction, cancelAction, stopAction) {
|
||||
if (dialog === "dialog") {
|
||||
document.getElementById("modal-footer").innerHTML = '';
|
||||
document.getElementById("modal-footer").appendChild(buttonGroup);
|
||||
const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
|
||||
dialog.show()
|
||||
dialog.on("hidden.bs.modal", () => {
|
||||
cancelAction()
|
||||
})
|
||||
const extensionDialog = document.getElementById("extension-dialog");
|
||||
const dialog = bootstrap.Modal.getOrCreateInstance(extensionDialog);
|
||||
dialog.show();
|
||||
|
||||
extensionDialog.addEventListener("hidden.bs.modal", cancelAction);
|
||||
// const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
|
||||
// dialog.show()
|
||||
// dialog.on("hidden.bs.modal", () => {
|
||||
// cancelAction()
|
||||
// })
|
||||
} else {
|
||||
form.appendChild(buttonGroup);
|
||||
}
|
||||
|
||||
@@ -35,11 +35,16 @@ function renderForm(json, dialog, submitAction, cancelAction, stopAction) {
|
||||
if (dialog === "dialog") {
|
||||
document.getElementById("modal-footer").innerHTML = '';
|
||||
document.getElementById("modal-footer").appendChild(buttonGroup);
|
||||
const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
|
||||
dialog.show()
|
||||
dialog.on("hidden.bs.modal", () => {
|
||||
cancelAction()
|
||||
})
|
||||
const extensionDialog = document.getElementById("extension-dialog");
|
||||
const dialog = bootstrap.Modal.getOrCreateInstance(extensionDialog);
|
||||
dialog.show();
|
||||
|
||||
extensionDialog.addEventListener("hidden.bs.modal", cancelAction);
|
||||
// const dialog = bootstrap.Modal.getOrCreateInstance("#extension-dialog");
|
||||
// dialog.show()
|
||||
// dialog.on("hidden.bs.modal", () => {
|
||||
// cancelAction()
|
||||
// })
|
||||
} else {
|
||||
form.appendChild(buttonGroup);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hiddify/hiddify-core/v2/common"
|
||||
"github.com/hiddify/hiddify-core/v2/db"
|
||||
|
||||
"github.com/hiddify/hiddify-core/v2/service_manager"
|
||||
)
|
||||
|
||||
@@ -26,12 +27,29 @@ func RegisterExtension(factory ExtensionFactory) error {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
table := db.GetTable[extensionData]()
|
||||
_, err := table.FirstOrInsert(func(data extensionData) bool { return data.Id == factory.Id }, func() extensionData { return extensionData{Id: factory.Id, Enable: false} })
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allExtensionsMap[factory.Id] = factory
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isEnable(id string) bool {
|
||||
table := db.GetTable[extensionData]()
|
||||
extdata, err := table.First(func(data extensionData) bool { return data.Id == id })
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return extdata.Enable
|
||||
}
|
||||
|
||||
func loadExtension(factory ExtensionFactory) error {
|
||||
if !isEnable(factory.Id) {
|
||||
return fmt.Errorf("Extension with ID %s is not enabled", factory.Id)
|
||||
}
|
||||
extension := factory.Builder()
|
||||
extension.init(factory.Id)
|
||||
|
||||
@@ -46,11 +64,18 @@ type extensionService struct {
|
||||
}
|
||||
|
||||
func (s *extensionService) Start() error {
|
||||
common.Storage.GetExtensionData("default", &generalExtensionData)
|
||||
|
||||
for id, factory := range allExtensionsMap {
|
||||
if val, ok := generalExtensionData.ExtensionStatusMap[id]; ok && val {
|
||||
loadExtension(factory)
|
||||
table := db.GetTable[extensionData]()
|
||||
extdata, err := table.Select(func(data extensionData) bool { return data.Enable })
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select enabled extensions: %w", err)
|
||||
}
|
||||
for _, data := range extdata {
|
||||
if factory, ok := allExtensionsMap[data.Id]; ok {
|
||||
if err := loadExtension(factory); err != nil {
|
||||
return fmt.Errorf("failed to load extension %s: %w", data.Id, err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("extension %s is enabled but not found", data.Id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
5
go.mod
5
go.mod
@@ -23,7 +23,10 @@ require (
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require github.com/hiddify/hiddify-app-demo-extension v0.0.0-20240929132536-e158b83e958c
|
||||
require (
|
||||
github.com/Yiwen-Chan/tinydb v0.0.0-20230129042445-3321642f0674
|
||||
github.com/hiddify/hiddify-app-demo-extension v0.0.0-20240929132536-e158b83e958c
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -18,6 +18,8 @@ github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGav
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/Yiwen-Chan/tinydb v0.0.0-20230129042445-3321642f0674 h1:Sf029Pn6NCxD0TP/AeEO87epoaNeCtUFrCHKndEc3G0=
|
||||
github.com/Yiwen-Chan/tinydb v0.0.0-20230129042445-3321642f0674/go.mod h1:FKpvt4bXlMiJn5DipBosCuM1tH27p0z9RI3sHRMH+40=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hiddify/hiddify-core/v2/service_manager"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/sagernet/bbolt"
|
||||
bboltErrors "github.com/sagernet/bbolt/errors"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
var (
|
||||
Storage CacheFile
|
||||
bucketExtension = []byte("extension")
|
||||
bucketHiddify = []byte("hiddify")
|
||||
|
||||
bucketNameList = []string{
|
||||
string(bucketExtension),
|
||||
string(bucketHiddify),
|
||||
}
|
||||
)
|
||||
|
||||
type StorageService struct {
|
||||
// Storage *CacheFile
|
||||
}
|
||||
|
||||
func (s *StorageService) Start() error {
|
||||
Storage = *NewStorage(context.Background(), option.CacheFileOptions{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StorageService) Close() error {
|
||||
if Storage.DB != nil {
|
||||
Storage.DB.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
service_manager.RegisterPreservice(&StorageService{})
|
||||
}
|
||||
|
||||
type CacheFile struct {
|
||||
ctx context.Context
|
||||
path string
|
||||
cacheID []byte
|
||||
|
||||
DB *bbolt.DB
|
||||
}
|
||||
|
||||
func NewStorage(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
||||
var path string
|
||||
if options.Path != "" {
|
||||
path = options.Path
|
||||
} else {
|
||||
path = "hiddify.db"
|
||||
}
|
||||
var cacheIDBytes []byte
|
||||
if options.CacheID != "" {
|
||||
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
|
||||
}
|
||||
cache := &CacheFile{
|
||||
ctx: ctx,
|
||||
path: filemanager.BasePath(ctx, path),
|
||||
cacheID: cacheIDBytes,
|
||||
}
|
||||
err := cache.start()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
func (c *CacheFile) start() error {
|
||||
const fileMode = 0o666
|
||||
options := bbolt.Options{Timeout: time.Second}
|
||||
var (
|
||||
db *bbolt.DB
|
||||
err error
|
||||
)
|
||||
for i := 0; i < 10; i++ {
|
||||
db, err = bbolt.Open(c.path, fileMode, &options)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if errors.Is(err, bboltErrors.ErrTimeout) {
|
||||
continue
|
||||
}
|
||||
if E.IsMulti(err, bboltErrors.ErrInvalid, bboltErrors.ErrChecksum, bboltErrors.ErrVersionMismatch) {
|
||||
rmErr := os.Remove(c.path)
|
||||
if rmErr != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = filemanager.Chown(c.ctx, c.path)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return E.Cause(err, "platform chown")
|
||||
}
|
||||
err = db.Batch(func(tx *bbolt.Tx) error {
|
||||
return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
|
||||
if name[0] == 0 {
|
||||
return b.ForEachBucket(func(k []byte) error {
|
||||
bucketName := string(k)
|
||||
if !(common.Contains(bucketNameList, bucketName)) {
|
||||
_ = b.DeleteBucket(name)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
bucketName := string(name)
|
||||
if !(common.Contains(bucketNameList, bucketName)) {
|
||||
_ = tx.DeleteBucket(name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return err
|
||||
}
|
||||
c.DB = db
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CacheFile) bucket(t *bbolt.Tx, key []byte) *bbolt.Bucket {
|
||||
if c.cacheID == nil {
|
||||
return t.Bucket(key)
|
||||
}
|
||||
bucket := t.Bucket(c.cacheID)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.Bucket(key)
|
||||
}
|
||||
|
||||
func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error) {
|
||||
if c.cacheID == nil {
|
||||
return t.CreateBucketIfNotExists(key)
|
||||
}
|
||||
bucket, err := t.CreateBucketIfNotExists(c.cacheID)
|
||||
if bucket == nil {
|
||||
return nil, err
|
||||
}
|
||||
return bucket.CreateBucketIfNotExists(key)
|
||||
}
|
||||
|
||||
func (c *CacheFile) GetExtensionData(extension_id string, default_value any) error {
|
||||
err := c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := c.bucket(t, bucketExtension)
|
||||
if bucket == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
setBinary := bucket.Get([]byte(extension_id))
|
||||
if len(setBinary) == 0 {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return json.Unmarshal(setBinary, &default_value)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveExtensionData(extension_id string, data any) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(t, bucketExtension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assuming T implements MarshalBinary
|
||||
|
||||
setBinary, err := json.MarshalIndent(data, " ", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bucket.Put([]byte(extension_id), setBinary)
|
||||
})
|
||||
}
|
||||
131
v2/db/db.go
Normal file
131
v2/db/db.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
tinydb "github.com/Yiwen-Chan/tinydb"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
tdb *tinydb.Database
|
||||
}
|
||||
|
||||
var instance map[string]*DB = make(map[string]*DB)
|
||||
|
||||
func Instance(name string) *DB {
|
||||
if db, ok := instance[name]; ok {
|
||||
return db
|
||||
}
|
||||
os.MkdirAll("data", 0o700)
|
||||
db, err := NewDB("data/hiddify-db-" + name + ".json")
|
||||
if err != nil {
|
||||
fmt.Println("Default DB instance failed", err)
|
||||
}
|
||||
instance[name] = db
|
||||
return db
|
||||
}
|
||||
|
||||
func NewDB(path string) (*DB, error) {
|
||||
storage, err := tinydb.JSONStorage(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tdb, err := tinydb.TinyDB(storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DB{
|
||||
tdb: tdb,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() error {
|
||||
return d.tdb.Close()
|
||||
}
|
||||
|
||||
func GetTableDB[T any](db *DB) *Table[T] {
|
||||
tt := tinydb.GetTable[T](db.tdb)
|
||||
if tt == nil {
|
||||
return nil
|
||||
}
|
||||
return &Table[T]{
|
||||
Table: tt,
|
||||
}
|
||||
}
|
||||
|
||||
func GetTable[T any]() *Table[T] {
|
||||
var t T
|
||||
name := reflect.TypeOf(t).Name()
|
||||
|
||||
tt := tinydb.GetTable[T](Instance(name).tdb)
|
||||
if tt == nil {
|
||||
return nil
|
||||
}
|
||||
return &Table[T]{
|
||||
Table: tt,
|
||||
}
|
||||
}
|
||||
|
||||
type Table[T any] struct {
|
||||
*tinydb.Table[T]
|
||||
}
|
||||
|
||||
func (tbl *Table[T]) Select(selector func(T) bool) ([]T, error) {
|
||||
return tbl.Table.Select(selector)
|
||||
}
|
||||
|
||||
func (tbl *Table[T]) All() ([]T, error) {
|
||||
return tbl.Table.Select(func(T) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (tbl *Table[T]) Insert(items ...T) error {
|
||||
return tbl.Table.Insert(items...)
|
||||
}
|
||||
|
||||
func (tbl *Table[T]) Delete(selector func(T) bool) ([]T, error) {
|
||||
return tbl.Table.Delete(selector)
|
||||
}
|
||||
|
||||
func (tbl *Table[T]) Update(update func(T) T, selector func(T) bool) error {
|
||||
return tbl.Table.Update(update, selector)
|
||||
}
|
||||
|
||||
func (tbl *Table[T]) First(selector func(T) bool) (*T, error) {
|
||||
data, err := tbl.Select(selector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
return &data[0], nil
|
||||
}
|
||||
|
||||
func (table *Table[T]) FirstOrInsert(selector func(d T) bool, generator func() T) (*T, error) {
|
||||
data, err := table.First(selector)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if err := table.Insert(generator()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return table.First(selector)
|
||||
}
|
||||
|
||||
func (table *Table[T]) ReplaceOrInsert(selector func(d T) bool, generator T) error {
|
||||
data, err := table.First(selector)
|
||||
if err == nil && data != nil {
|
||||
if _, err := table.Delete(selector); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := table.Insert(generator); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user