mirror of
https://github.com/anyproto/any-sync.git
synced 2025-06-08 05:57:03 +09:00
wip: drpc access log
This commit is contained in:
parent
6ed21a94d3
commit
ed0a751d15
5 changed files with 763 additions and 34 deletions
|
@ -3,6 +3,7 @@ package commonspace
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/anytypeio/any-sync/accountservice"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/headsync"
|
||||
|
@ -20,6 +21,7 @@ import (
|
|||
"github.com/anytypeio/any-sync/commonspace/spacestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/metric"
|
||||
"github.com/anytypeio/any-sync/net/peer"
|
||||
"github.com/anytypeio/any-sync/nodeconf"
|
||||
"github.com/anytypeio/any-sync/util/crypto"
|
||||
|
@ -29,7 +31,6 @@ import (
|
|||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
@ -55,10 +56,21 @@ type SpaceCreatePayload struct {
|
|||
}
|
||||
|
||||
type HandleMessage struct {
|
||||
Id uint64
|
||||
Deadline time.Time
|
||||
SenderId string
|
||||
Message *spacesyncproto.ObjectSyncMessage
|
||||
Id uint64
|
||||
ReceiveTime time.Time
|
||||
StartHandlingTime time.Time
|
||||
Deadline time.Time
|
||||
SenderId string
|
||||
Message *spacesyncproto.ObjectSyncMessage
|
||||
}
|
||||
|
||||
func (m HandleMessage) LogFields(fields ...zap.Field) []zap.Field {
|
||||
return append(fields,
|
||||
metric.SpaceId(m.Message.SpaceId),
|
||||
metric.ObjectId(m.Message.ObjectId),
|
||||
metric.QueueDur(m.StartHandlingTime.Sub(m.ReceiveTime)),
|
||||
metric.TotalDur(time.Since(m.ReceiveTime)),
|
||||
)
|
||||
}
|
||||
|
||||
type SpaceDerivePayload struct {
|
||||
|
@ -77,7 +89,7 @@ type SpaceDescription struct {
|
|||
}
|
||||
|
||||
func NewSpaceId(id string, repKey uint64) string {
|
||||
return strings.Join([]string{id, strconv.FormatUint(repKey, 36)}, ".")
|
||||
return fmt.Sprintf("%s.%s", id, strconv.FormatUint(repKey, 36))
|
||||
}
|
||||
|
||||
type Space interface {
|
||||
|
@ -88,6 +100,7 @@ type Space interface {
|
|||
DebugAllHeads() []headsync.TreeHeads
|
||||
Description() (SpaceDescription, error)
|
||||
|
||||
DeriveTree(ctx context.Context, payload objecttree.ObjectTreeCreatePayload) (res treestorage.TreeStorageCreatePayload, err error)
|
||||
CreateTree(ctx context.Context, payload objecttree.ObjectTreeCreatePayload) (res treestorage.TreeStorageCreatePayload, err error)
|
||||
PutTree(ctx context.Context, payload treestorage.TreeStorageCreatePayload, listener updatelistener.UpdateListener) (t objecttree.ObjectTree, err error)
|
||||
BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t objecttree.ObjectTree, err error)
|
||||
|
@ -117,13 +130,13 @@ type space struct {
|
|||
headSync headsync.HeadSync
|
||||
syncStatus syncstatus.StatusUpdater
|
||||
storage spacestorage.SpaceStorage
|
||||
treeManager *commonGetter
|
||||
cache *commonGetter
|
||||
account accountservice.Service
|
||||
aclList *syncacl.SyncAcl
|
||||
configuration nodeconf.Configuration
|
||||
configuration nodeconf.NodeConf
|
||||
settingsObject settings.SettingsObject
|
||||
peerManager peermanager.PeerManager
|
||||
treeBuilder objecttree.BuildObjectTreeFunc
|
||||
metric metric.Metric
|
||||
|
||||
handleQueue multiqueue.MultiQueue[HandleMessage]
|
||||
|
||||
|
@ -178,8 +191,8 @@ func (s *space) Init(ctx context.Context) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.aclList = syncacl.NewSyncAcl(aclList, s.objectSync.SyncClient().MessagePool())
|
||||
s.treeManager.AddObject(s.aclList)
|
||||
s.aclList = syncacl.NewSyncAcl(aclList, s.objectSync.MessagePool())
|
||||
s.cache.AddObject(s.aclList)
|
||||
|
||||
deletionState := settingsstate.NewObjectDeletionState(s.storage)
|
||||
deps := settings.Deps{
|
||||
|
@ -187,8 +200,6 @@ func (s *space) Init(ctx context.Context) (err error) {
|
|||
res, err := s.BuildTree(ctx, id, BuildTreeOpts{
|
||||
Listener: listener,
|
||||
WaitTreeRemoteSync: false,
|
||||
// space settings document should not have empty data
|
||||
treeBuilder: objecttree.BuildObjectTree,
|
||||
})
|
||||
log.Debug("building settings tree", zap.String("id", id), zap.String("spaceId", s.id))
|
||||
if err != nil {
|
||||
|
@ -198,7 +209,7 @@ func (s *space) Init(ctx context.Context) (err error) {
|
|||
return
|
||||
},
|
||||
Account: s.account,
|
||||
TreeManager: s.treeManager,
|
||||
TreeGetter: s.cache,
|
||||
Store: s.storage,
|
||||
DeletionState: deletionState,
|
||||
Provider: s.headSync,
|
||||
|
@ -206,12 +217,13 @@ func (s *space) Init(ctx context.Context) (err error) {
|
|||
OnSpaceDelete: s.onSpaceDelete,
|
||||
}
|
||||
s.settingsObject = settings.NewSettingsObject(deps, s.id)
|
||||
s.objectSync.Init()
|
||||
s.headSync.Init(initialIds, deletionState)
|
||||
err = s.settingsObject.Init(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.treeManager.AddObject(s.settingsObject)
|
||||
s.cache.AddObject(s.settingsObject)
|
||||
s.syncStatus.Run()
|
||||
s.handleQueue = multiqueue.New[HandleMessage](s.handleMessage, 100)
|
||||
return nil
|
||||
|
@ -243,6 +255,23 @@ func (s *space) DebugAllHeads() []headsync.TreeHeads {
|
|||
return s.headSync.DebugAllHeads()
|
||||
}
|
||||
|
||||
func (s *space) DeriveTree(ctx context.Context, payload objecttree.ObjectTreeCreatePayload) (res treestorage.TreeStorageCreatePayload, err error) {
|
||||
if s.isClosed.Load() {
|
||||
err = ErrSpaceClosed
|
||||
return
|
||||
}
|
||||
root, err := objecttree.DeriveObjectTreeRoot(payload, s.aclList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = treestorage.TreeStorageCreatePayload{
|
||||
RootRawChange: root,
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{root},
|
||||
Heads: []string{root.Id},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *space) CreateTree(ctx context.Context, payload objecttree.ObjectTreeCreatePayload) (res treestorage.TreeStorageCreatePayload, err error) {
|
||||
if s.isClosed.Load() {
|
||||
err = ErrSpaceClosed
|
||||
|
@ -267,17 +296,16 @@ func (s *space) PutTree(ctx context.Context, payload treestorage.TreeStorageCrea
|
|||
return
|
||||
}
|
||||
deps := synctree.BuildDeps{
|
||||
SpaceId: s.id,
|
||||
SyncClient: s.objectSync.SyncClient(),
|
||||
Configuration: s.configuration,
|
||||
HeadNotifiable: s.headSync,
|
||||
Listener: listener,
|
||||
AclList: s.aclList,
|
||||
SpaceStorage: s.storage,
|
||||
OnClose: s.onObjectClose,
|
||||
SyncStatus: s.syncStatus,
|
||||
PeerGetter: s.peerManager,
|
||||
BuildObjectTree: s.treeBuilder,
|
||||
SpaceId: s.id,
|
||||
ObjectSync: s.objectSync,
|
||||
Configuration: s.configuration,
|
||||
HeadNotifiable: s.headSync,
|
||||
Listener: listener,
|
||||
AclList: s.aclList,
|
||||
SpaceStorage: s.storage,
|
||||
OnClose: s.onObjectClose,
|
||||
SyncStatus: s.syncStatus,
|
||||
PeerGetter: s.peerManager,
|
||||
}
|
||||
t, err = synctree.PutSyncTree(ctx, payload, deps)
|
||||
if err != nil {
|
||||
|
@ -291,7 +319,6 @@ func (s *space) PutTree(ctx context.Context, payload treestorage.TreeStorageCrea
|
|||
type BuildTreeOpts struct {
|
||||
Listener updatelistener.UpdateListener
|
||||
WaitTreeRemoteSync bool
|
||||
treeBuilder objecttree.BuildObjectTreeFunc
|
||||
}
|
||||
|
||||
type HistoryTreeOpts struct {
|
||||
|
@ -304,13 +331,10 @@ func (s *space) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t
|
|||
err = ErrSpaceClosed
|
||||
return
|
||||
}
|
||||
treeBuilder := opts.treeBuilder
|
||||
if treeBuilder == nil {
|
||||
treeBuilder = s.treeBuilder
|
||||
}
|
||||
|
||||
deps := synctree.BuildDeps{
|
||||
SpaceId: s.id,
|
||||
SyncClient: s.objectSync.SyncClient(),
|
||||
ObjectSync: s.objectSync,
|
||||
Configuration: s.configuration,
|
||||
HeadNotifiable: s.headSync,
|
||||
Listener: opts.Listener,
|
||||
|
@ -320,7 +344,6 @@ func (s *space) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t
|
|||
SyncStatus: s.syncStatus,
|
||||
WaitTreeRemoteSync: opts.WaitTreeRemoteSync,
|
||||
PeerGetter: s.peerManager,
|
||||
BuildObjectTree: treeBuilder,
|
||||
}
|
||||
if t, err = synctree.BuildSyncTreeOrGetRemote(ctx, id, deps); err != nil {
|
||||
return nil, err
|
||||
|
@ -362,6 +385,7 @@ func (s *space) DeleteSpace(ctx context.Context, deleteChange *treechangeproto.R
|
|||
|
||||
func (s *space) HandleMessage(ctx context.Context, hm HandleMessage) (err error) {
|
||||
threadId := hm.Message.ObjectId
|
||||
hm.ReceiveTime = time.Now()
|
||||
if hm.Message.ReplyId != "" {
|
||||
threadId += hm.Message.ReplyId
|
||||
defer func() {
|
||||
|
@ -378,12 +402,19 @@ func (s *space) HandleMessage(ctx context.Context, hm HandleMessage) (err error)
|
|||
}
|
||||
|
||||
func (s *space) handleMessage(msg HandleMessage) {
|
||||
var err error
|
||||
msg.StartHandlingTime = time.Now()
|
||||
ctx := peer.CtxWithPeerId(context.Background(), msg.SenderId)
|
||||
ctx = logger.CtxWithFields(ctx, zap.Uint64("msgId", msg.Id), zap.String("senderId", msg.SenderId))
|
||||
defer func() {
|
||||
s.metric.RequestLog(ctx, msg.LogFields(zap.Error(err))...)
|
||||
}()
|
||||
|
||||
if !msg.Deadline.IsZero() {
|
||||
now := time.Now()
|
||||
if now.After(msg.Deadline) {
|
||||
log.InfoCtx(ctx, "skip message: deadline exceed")
|
||||
err = context.DeadlineExceeded
|
||||
return
|
||||
}
|
||||
var cancel context.CancelFunc
|
||||
|
@ -391,7 +422,7 @@ func (s *space) handleMessage(msg HandleMessage) {
|
|||
defer cancel()
|
||||
}
|
||||
|
||||
if err := s.objectSync.HandleMessage(ctx, msg.SenderId, msg.Message); err != nil {
|
||||
if err = s.objectSync.HandleMessage(ctx, msg.SenderId, msg.Message); err != nil {
|
||||
if msg.Message.ObjectId != "" {
|
||||
// cleanup thread on error
|
||||
_ = s.handleQueue.CloseThread(msg.Message.ObjectId)
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/anytypeio/any-sync/commonspace/spacestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/metric"
|
||||
"github.com/anytypeio/any-sync/net/peer"
|
||||
"github.com/anytypeio/any-sync/net/pool"
|
||||
"github.com/anytypeio/any-sync/net/rpc/rpcerr"
|
||||
|
@ -52,6 +53,7 @@ type spaceService struct {
|
|||
credentialProvider credentialprovider.CredentialProvider
|
||||
treeManager treemanager.TreeManager
|
||||
pool pool.Pool
|
||||
metric metric.Metric
|
||||
}
|
||||
|
||||
func (s *spaceService) Init(a *app.App) (err error) {
|
||||
|
@ -68,6 +70,7 @@ func (s *spaceService) Init(a *app.App) (err error) {
|
|||
s.credentialProvider = credentialprovider.NewNoOp()
|
||||
}
|
||||
s.pool = a.MustComponent(pool.CName).(pool.Pool)
|
||||
s.metric, _ = a.Component(metric.CName).(metric.Metric)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -181,6 +184,7 @@ func (s *spaceService) NewSpace(ctx context.Context, id string) (Space, error) {
|
|||
treeBuilder: builder,
|
||||
isClosed: spaceIsClosed,
|
||||
isDeleted: spaceIsDeleted,
|
||||
metric: s.metric,
|
||||
}
|
||||
return sp, nil
|
||||
}
|
||||
|
|
|
@ -1,2 +1,656 @@
|
|||
package spacestorage
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/util/cidutil"
|
||||
"github.com/anytypeio/any-sync/util/crypto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
rand2 "golang.org/x/exp/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSuccessHeaderPayloadForSpaceCreate(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
_, rawHeaderWithId, err := rawHeaderWithId(accountKeys)
|
||||
require.NoError(t, err)
|
||||
err = validateCreateSpaceHeaderPayload(rawHeaderWithId)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFailedHeaderPayloadForSpaceCreate_InvalidFormatSpaceId(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
spaceHeaderSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderSeed)
|
||||
require.NoError(t, err)
|
||||
spaceHeaderPayload := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderPayload)
|
||||
require.NoError(t, err)
|
||||
replicationKey := rand2.Uint64()
|
||||
header := &spacesyncproto.SpaceHeader{
|
||||
Identity: identity,
|
||||
Timestamp: time.Now().Unix(),
|
||||
SpaceType: "SpaceType",
|
||||
ReplicationKey: replicationKey,
|
||||
Seed: spaceHeaderSeed,
|
||||
SpaceHeaderPayload: spaceHeaderPayload,
|
||||
}
|
||||
marhalled, err := header.Marshal()
|
||||
require.NoError(t, err)
|
||||
signature, err := accountKeys.SignKey.Sign(marhalled)
|
||||
require.NoError(t, err)
|
||||
rawHeader := &spacesyncproto.RawSpaceHeader{
|
||||
SpaceHeader: marhalled,
|
||||
Signature: signature,
|
||||
}
|
||||
marhalledRawHeader, err := rawHeader.Marshal()
|
||||
require.NoError(t, err)
|
||||
id, err := cidutil.NewCidFromBytes(marhalled)
|
||||
require.NoError(t, err)
|
||||
spaceId := fmt.Sprintf("%s%s", id, strconv.FormatUint(replicationKey, 36))
|
||||
rawHeaderWithId := &spacesyncproto.RawSpaceHeaderWithId{
|
||||
RawHeader: marhalledRawHeader,
|
||||
Id: spaceId,
|
||||
}
|
||||
err = validateCreateSpaceHeaderPayload(rawHeaderWithId)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func TestFailedHeaderPayloadForSpaceCreate_CidIsWrong(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
spaceHeaderSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderSeed)
|
||||
require.NoError(t, err)
|
||||
spaceHeaderPayload := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderPayload)
|
||||
require.NoError(t, err)
|
||||
replicationKey := rand2.Uint64()
|
||||
header := &spacesyncproto.SpaceHeader{
|
||||
Identity: identity,
|
||||
Timestamp: time.Now().Unix(),
|
||||
SpaceType: "SpaceType",
|
||||
ReplicationKey: replicationKey,
|
||||
Seed: spaceHeaderSeed,
|
||||
SpaceHeaderPayload: spaceHeaderPayload,
|
||||
}
|
||||
marhalled, err := header.Marshal()
|
||||
require.NoError(t, err)
|
||||
signature, err := accountKeys.SignKey.Sign(marhalled)
|
||||
require.NoError(t, err)
|
||||
rawHeader := &spacesyncproto.RawSpaceHeader{
|
||||
SpaceHeader: marhalled,
|
||||
Signature: signature,
|
||||
}
|
||||
marhalledRawHeader, err := rawHeader.Marshal()
|
||||
require.NoError(t, err)
|
||||
id := "faisdfjpiocpoakopkop34"
|
||||
spaceId := fmt.Sprintf("%s.%s", id, strconv.FormatUint(replicationKey, 36))
|
||||
rawHeaderWithId := &spacesyncproto.RawSpaceHeaderWithId{
|
||||
RawHeader: marhalledRawHeader,
|
||||
Id: spaceId,
|
||||
}
|
||||
err = validateCreateSpaceHeaderPayload(rawHeaderWithId)
|
||||
assert.EqualErrorf(t, err, objecttree.ErrIncorrectCid.Error(), "Error should be: %v, got: %v", objecttree.ErrIncorrectCid, err)
|
||||
}
|
||||
|
||||
func TestFailedHeaderPayloadForSpaceCreate_SignedWithAnotherIdentity(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
spaceHeaderSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderSeed)
|
||||
require.NoError(t, err)
|
||||
spaceHeaderPayload := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderPayload)
|
||||
require.NoError(t, err)
|
||||
replicationKey := rand2.Uint64()
|
||||
header := &spacesyncproto.SpaceHeader{
|
||||
Identity: identity,
|
||||
Timestamp: time.Now().Unix(),
|
||||
SpaceType: "SpaceType",
|
||||
ReplicationKey: replicationKey,
|
||||
Seed: spaceHeaderSeed,
|
||||
SpaceHeaderPayload: spaceHeaderPayload,
|
||||
}
|
||||
marhalled, err := header.Marshal()
|
||||
require.NoError(t, err)
|
||||
anotherAccountKeys, err := accountdata.NewRandom()
|
||||
signature, err := anotherAccountKeys.SignKey.Sign(marhalled)
|
||||
require.NoError(t, err)
|
||||
rawHeader := &spacesyncproto.RawSpaceHeader{
|
||||
SpaceHeader: marhalled,
|
||||
Signature: signature,
|
||||
}
|
||||
marhalledRawHeader, err := rawHeader.Marshal()
|
||||
require.NoError(t, err)
|
||||
id := "faisdfjpiocpoakopkop34"
|
||||
spaceId := fmt.Sprintf("%s.%s", id, strconv.FormatUint(replicationKey, 36))
|
||||
rawHeaderWithId := &spacesyncproto.RawSpaceHeaderWithId{
|
||||
RawHeader: marhalledRawHeader,
|
||||
Id: spaceId,
|
||||
}
|
||||
err = validateCreateSpaceHeaderPayload(rawHeaderWithId)
|
||||
assert.EqualErrorf(t, err, objecttree.ErrIncorrectCid.Error(), "Error should be: %v, got: %v", objecttree.ErrIncorrectCid, err)
|
||||
}
|
||||
|
||||
func TestSuccessAclPayloadSpace(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
spaceId := "AnySpaceId"
|
||||
_, rawWithId, err := rawAclWithId(accountKeys, spaceId)
|
||||
require.NoError(t, err)
|
||||
validationSpaceId, err := validateCreateSpaceAclPayload(rawWithId)
|
||||
require.Equal(t, validationSpaceId, spaceId)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFailAclPayloadSpace_IncorrectCid(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
readKeyBytes := make([]byte, 32)
|
||||
_, err = rand.Read(readKeyBytes)
|
||||
require.NoError(t, err)
|
||||
readKey, err := accountKeys.SignKey.GetPublic().Encrypt(readKeyBytes)
|
||||
require.NoError(t, err)
|
||||
masterKey, _, err := crypto.GenerateRandomEd25519KeyPair()
|
||||
require.NoError(t, err)
|
||||
rawIdentity, err := accountKeys.SignKey.GetPublic().Raw()
|
||||
require.NoError(t, err)
|
||||
identitySignature, err := masterKey.Sign(rawIdentity)
|
||||
require.NoError(t, err)
|
||||
rawMasterKey, err := masterKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
aclRoot := aclrecordproto.AclRoot{
|
||||
Identity: identity,
|
||||
MasterKey: rawMasterKey,
|
||||
SpaceId: "SpaceId",
|
||||
EncryptedReadKey: readKey,
|
||||
Timestamp: time.Now().Unix(),
|
||||
IdentitySignature: identitySignature,
|
||||
}
|
||||
marshalled, err := aclRoot.Marshal()
|
||||
require.NoError(t, err)
|
||||
signature, err := accountKeys.SignKey.Sign(marshalled)
|
||||
rawAclRecord := &aclrecordproto.RawAclRecord{
|
||||
Payload: marshalled,
|
||||
Signature: signature,
|
||||
}
|
||||
marshalledRaw, err := rawAclRecord.Marshal()
|
||||
require.NoError(t, err)
|
||||
aclHeadId := "rand"
|
||||
rawWithId := &aclrecordproto.RawAclRecordWithId{
|
||||
Payload: marshalledRaw,
|
||||
Id: aclHeadId,
|
||||
}
|
||||
_, err = validateCreateSpaceAclPayload(rawWithId)
|
||||
assert.EqualErrorf(t, err, objecttree.ErrIncorrectCid.Error(), "Error should be: %v, got: %v", objecttree.ErrIncorrectCid, err)
|
||||
}
|
||||
|
||||
func TestFailedAclPayloadSpace_IncorrectSignature(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
readKeyBytes := make([]byte, 32)
|
||||
_, err = rand.Read(readKeyBytes)
|
||||
require.NoError(t, err)
|
||||
readKey, err := accountKeys.SignKey.GetPublic().Encrypt(readKeyBytes)
|
||||
require.NoError(t, err)
|
||||
masterKey, _, err := crypto.GenerateRandomEd25519KeyPair()
|
||||
require.NoError(t, err)
|
||||
rawIdentity, err := accountKeys.SignKey.GetPublic().Raw()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
identitySignature, err := masterKey.Sign(rawIdentity)
|
||||
require.NoError(t, err)
|
||||
rawMasterKey, err := masterKey.GetPublic().Raw()
|
||||
require.NoError(t, err)
|
||||
aclRoot := aclrecordproto.AclRoot{
|
||||
Identity: identity,
|
||||
MasterKey: rawMasterKey,
|
||||
SpaceId: "SpaceId",
|
||||
EncryptedReadKey: readKey,
|
||||
Timestamp: time.Now().Unix(),
|
||||
IdentitySignature: identitySignature,
|
||||
}
|
||||
marshalled, err := aclRoot.Marshal()
|
||||
require.NoError(t, err)
|
||||
rawAclRecord := &aclrecordproto.RawAclRecord{
|
||||
Payload: marshalled,
|
||||
Signature: marshalled,
|
||||
}
|
||||
marshalledRaw, err := rawAclRecord.Marshal()
|
||||
require.NoError(t, err)
|
||||
aclHeadId, err := cidutil.NewCidFromBytes(marshalledRaw)
|
||||
require.NoError(t, err)
|
||||
rawWithId := &aclrecordproto.RawAclRecordWithId{
|
||||
Payload: marshalledRaw,
|
||||
Id: aclHeadId,
|
||||
}
|
||||
_, err = validateCreateSpaceAclPayload(rawWithId)
|
||||
assert.NotNil(t, err)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func TestFailedAclPayloadSpace_IncorrectIdentitySignature(t *testing.T) {
|
||||
spaceId := "AnySpaceId"
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
readKeyBytes := make([]byte, 32)
|
||||
_, err = rand.Read(readKeyBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
readKey, err := accountKeys.SignKey.GetPublic().Encrypt(readKeyBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
masterKey, _, err := crypto.GenerateRandomEd25519KeyPair()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
masterPubKey := masterKey.GetPublic()
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawMasterKey, err := masterPubKey.Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclRoot := aclrecordproto.AclRoot{
|
||||
Identity: identity,
|
||||
MasterKey: rawMasterKey,
|
||||
SpaceId: spaceId,
|
||||
EncryptedReadKey: readKey,
|
||||
Timestamp: time.Now().Unix(),
|
||||
IdentitySignature: identity,
|
||||
}
|
||||
marshalled, err := aclRoot.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := accountKeys.SignKey.Sign(marshalled)
|
||||
rawAclRecord := &aclrecordproto.RawAclRecord{
|
||||
Payload: marshalled,
|
||||
Signature: signature,
|
||||
}
|
||||
marshalledRaw, err := rawAclRecord.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclHeadId, err := cidutil.NewCidFromBytes(marshalledRaw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawWithId := &aclrecordproto.RawAclRecordWithId{
|
||||
Payload: marshalledRaw,
|
||||
Id: aclHeadId,
|
||||
}
|
||||
_, err = validateCreateSpaceAclPayload(rawWithId)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func TestSuccessSettingsPayloadSpace(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
spaceSettingsSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceSettingsSeed)
|
||||
require.NoError(t, err)
|
||||
changePayload := make([]byte, 32)
|
||||
_, err = rand.Read(changePayload)
|
||||
require.NoError(t, err)
|
||||
spaceId := "SpaceId"
|
||||
rootChange := &treechangeproto.RootChange{
|
||||
AclHeadId: "AclHeadId",
|
||||
SpaceId: spaceId,
|
||||
ChangeType: "ChangeType",
|
||||
Timestamp: time.Now().Unix(),
|
||||
Seed: spaceSettingsSeed,
|
||||
Identity: identity,
|
||||
ChangePayload: changePayload,
|
||||
}
|
||||
marshalledChange, err := rootChange.Marshal()
|
||||
require.NoError(t, err)
|
||||
signature, err := accountKeys.SignKey.Sign(marshalledChange)
|
||||
require.NoError(t, err)
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: marshalledChange,
|
||||
Signature: signature,
|
||||
}
|
||||
marshalledRawChange, err := raw.Marshal()
|
||||
id, err := cidutil.NewCidFromBytes(marshalledRawChange)
|
||||
require.NoError(t, err)
|
||||
rawIdChange := &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: marshalledRawChange,
|
||||
Id: id,
|
||||
}
|
||||
_, validationSpaceId, err := validateCreateSpaceSettingsPayload(rawIdChange)
|
||||
require.Equal(t, validationSpaceId, spaceId)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFailSettingsPayloadSpace_InvalidSignature(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
spaceSettingsSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceSettingsSeed)
|
||||
require.NoError(t, err)
|
||||
changePayload := make([]byte, 32)
|
||||
_, err = rand.Read(changePayload)
|
||||
require.NoError(t, err)
|
||||
rootChange := &treechangeproto.RootChange{
|
||||
AclHeadId: "AclHeadId",
|
||||
SpaceId: "SpaceId",
|
||||
ChangeType: "ChangeType",
|
||||
Timestamp: time.Now().Unix(),
|
||||
Seed: spaceSettingsSeed,
|
||||
Identity: identity,
|
||||
ChangePayload: changePayload,
|
||||
}
|
||||
marshalledChange, err := rootChange.Marshal()
|
||||
require.NoError(t, err)
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: marshalledChange,
|
||||
Signature: marshalledChange,
|
||||
}
|
||||
marshalledRawChange, err := raw.Marshal()
|
||||
id, err := cidutil.NewCidFromBytes(marshalledRawChange)
|
||||
require.NoError(t, err)
|
||||
rawIdChange := &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: marshalledRawChange,
|
||||
Id: id,
|
||||
}
|
||||
_, _, err = validateCreateSpaceSettingsPayload(rawIdChange)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func TestFailSettingsPayloadSpace_InvalidCid(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
require.NoError(t, err)
|
||||
spaceSettingsSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceSettingsSeed)
|
||||
require.NoError(t, err)
|
||||
changePayload := make([]byte, 32)
|
||||
_, err = rand.Read(changePayload)
|
||||
require.NoError(t, err)
|
||||
rootChange := &treechangeproto.RootChange{
|
||||
AclHeadId: "AclHeadId",
|
||||
SpaceId: "SpaceId",
|
||||
ChangeType: "ChangeType",
|
||||
Timestamp: time.Now().Unix(),
|
||||
Seed: spaceSettingsSeed,
|
||||
Identity: identity,
|
||||
ChangePayload: changePayload,
|
||||
}
|
||||
marshalledChange, err := rootChange.Marshal()
|
||||
require.NoError(t, err)
|
||||
signature, err := accountKeys.SignKey.Sign(marshalledChange)
|
||||
require.NoError(t, err)
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: marshalledChange,
|
||||
Signature: signature,
|
||||
}
|
||||
marshalledRawChange, err := raw.Marshal()
|
||||
id := "id"
|
||||
require.NoError(t, err)
|
||||
rawIdChange := &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: marshalledRawChange,
|
||||
Id: id,
|
||||
}
|
||||
_, _, err = validateCreateSpaceSettingsPayload(rawIdChange)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func TestSuccessSameIds(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
spaceId, rawHeaderWithId, err := rawHeaderWithId(accountKeys)
|
||||
require.NoError(t, err)
|
||||
aclHeadId, rawAclWithId, err := rawAclWithId(accountKeys, spaceId)
|
||||
require.NoError(t, err)
|
||||
rawSettingsPayload, err := rawSettingsPayload(accountKeys, spaceId, aclHeadId)
|
||||
spacePayload := SpaceStorageCreatePayload{
|
||||
AclWithId: rawAclWithId,
|
||||
SpaceHeaderWithId: rawHeaderWithId,
|
||||
SpaceSettingsWithId: rawSettingsPayload,
|
||||
}
|
||||
err = ValidateSpaceStorageCreatePayload(spacePayload)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFailWithAclWrongSpaceId(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
spaceId, rawHeaderWithId, err := rawHeaderWithId(accountKeys)
|
||||
require.NoError(t, err)
|
||||
aclHeadId, rawAclWithId, err := rawAclWithId(accountKeys, "spaceId")
|
||||
require.NoError(t, err)
|
||||
rawSettingsPayload, err := rawSettingsPayload(accountKeys, spaceId, aclHeadId)
|
||||
spacePayload := SpaceStorageCreatePayload{
|
||||
AclWithId: rawAclWithId,
|
||||
SpaceHeaderWithId: rawHeaderWithId,
|
||||
SpaceSettingsWithId: rawSettingsPayload,
|
||||
}
|
||||
err = ValidateSpaceStorageCreatePayload(spacePayload)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func TestFailWithSettingsWrongSpaceId(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
spaceId, rawHeaderWithId, err := rawHeaderWithId(accountKeys)
|
||||
require.NoError(t, err)
|
||||
aclHeadId, rawAclWithId, err := rawAclWithId(accountKeys, spaceId)
|
||||
require.NoError(t, err)
|
||||
rawSettingsPayload, err := rawSettingsPayload(accountKeys, "spaceId", aclHeadId)
|
||||
spacePayload := SpaceStorageCreatePayload{
|
||||
AclWithId: rawAclWithId,
|
||||
SpaceHeaderWithId: rawHeaderWithId,
|
||||
SpaceSettingsWithId: rawSettingsPayload,
|
||||
}
|
||||
err = ValidateSpaceStorageCreatePayload(spacePayload)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func TestFailWithWrongAclHeadIdInSettingsPayload(t *testing.T) {
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
spaceId, rawHeaderWithId, err := rawHeaderWithId(accountKeys)
|
||||
require.NoError(t, err)
|
||||
_, rawAclWithId, err := rawAclWithId(accountKeys, spaceId)
|
||||
require.NoError(t, err)
|
||||
rawSettingsPayload, err := rawSettingsPayload(accountKeys, spaceId, "aclHeadId")
|
||||
spacePayload := SpaceStorageCreatePayload{
|
||||
AclWithId: rawAclWithId,
|
||||
SpaceHeaderWithId: rawHeaderWithId,
|
||||
SpaceSettingsWithId: rawSettingsPayload,
|
||||
}
|
||||
err = ValidateSpaceStorageCreatePayload(spacePayload)
|
||||
assert.EqualErrorf(t, err, ErrIncorrectSpaceHeader.Error(), "Error should be: %v, got: %v", ErrIncorrectSpaceHeader, err)
|
||||
}
|
||||
|
||||
func rawSettingsPayload(accountKeys *accountdata.AccountKeys, spaceId, aclHeadId string) (rawIdChange *treechangeproto.RawTreeChangeWithId, err error) {
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
spaceSettingsSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceSettingsSeed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
changePayload := make([]byte, 32)
|
||||
_, err = rand.Read(changePayload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rootChange := &treechangeproto.RootChange{
|
||||
AclHeadId: aclHeadId,
|
||||
SpaceId: spaceId,
|
||||
ChangeType: "ChangeType",
|
||||
Timestamp: time.Now().Unix(),
|
||||
Seed: spaceSettingsSeed,
|
||||
Identity: identity,
|
||||
ChangePayload: changePayload,
|
||||
}
|
||||
marshalledChange, err := rootChange.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := accountKeys.SignKey.Sign(marshalledChange)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: marshalledChange,
|
||||
Signature: signature,
|
||||
}
|
||||
marshalledRawChange, err := raw.Marshal()
|
||||
id, err := cidutil.NewCidFromBytes(marshalledRawChange)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawIdChange = &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: marshalledRawChange,
|
||||
Id: id,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func rawAclWithId(accountKeys *accountdata.AccountKeys, spaceId string) (aclHeadId string, rawWithId *aclrecordproto.RawAclRecordWithId, err error) {
|
||||
readKeyBytes := make([]byte, 32)
|
||||
_, err = rand.Read(readKeyBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
readKey, err := accountKeys.SignKey.GetPublic().Encrypt(readKeyBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
masterKey, _, err := crypto.GenerateRandomEd25519KeyPair()
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
masterPubKey := masterKey.GetPublic()
|
||||
rawIdentity, err := accountKeys.SignKey.GetPublic().Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
identitySignature, err := masterKey.Sign(rawIdentity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawMasterKey, err := masterPubKey.Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclRoot := aclrecordproto.AclRoot{
|
||||
Identity: identity,
|
||||
MasterKey: rawMasterKey,
|
||||
SpaceId: spaceId,
|
||||
EncryptedReadKey: readKey,
|
||||
Timestamp: time.Now().Unix(),
|
||||
IdentitySignature: identitySignature,
|
||||
}
|
||||
marshalled, err := aclRoot.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := accountKeys.SignKey.Sign(marshalled)
|
||||
rawAclRecord := &aclrecordproto.RawAclRecord{
|
||||
Payload: marshalled,
|
||||
Signature: signature,
|
||||
}
|
||||
marshalledRaw, err := rawAclRecord.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclHeadId, err = cidutil.NewCidFromBytes(marshalledRaw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawWithId = &aclrecordproto.RawAclRecordWithId{
|
||||
Payload: marshalledRaw,
|
||||
Id: aclHeadId,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func rawHeaderWithId(accountKeys *accountdata.AccountKeys) (spaceId string, rawWithId *spacesyncproto.RawSpaceHeaderWithId, err error) {
|
||||
identity, err := accountKeys.SignKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
spaceHeaderSeed := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderSeed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
spaceHeaderPayload := make([]byte, 32)
|
||||
_, err = rand.Read(spaceHeaderPayload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
replicationKey := rand2.Uint64()
|
||||
header := &spacesyncproto.SpaceHeader{
|
||||
Identity: identity,
|
||||
Timestamp: time.Now().Unix(),
|
||||
SpaceType: "SpaceType",
|
||||
ReplicationKey: replicationKey,
|
||||
Seed: spaceHeaderSeed,
|
||||
SpaceHeaderPayload: spaceHeaderPayload,
|
||||
}
|
||||
marhalled, err := header.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := accountKeys.SignKey.Sign(marhalled)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawHeader := &spacesyncproto.RawSpaceHeader{
|
||||
SpaceHeader: marhalled,
|
||||
Signature: signature,
|
||||
}
|
||||
marhalledRawHeader, err := rawHeader.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id, err := cidutil.NewCidFromBytes(marhalledRawHeader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
spaceId = fmt.Sprintf("%s.%s", id, strconv.FormatUint(replicationKey, 36))
|
||||
rawWithId = &spacesyncproto.RawSpaceHeaderWithId{
|
||||
RawHeader: marhalledRawHeader,
|
||||
Id: spaceId,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
39
metric/log.go
Normal file
39
metric/log.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package metric
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Method(val string) zap.Field {
|
||||
return zap.String("rpc", val)
|
||||
}
|
||||
|
||||
func QueueDur(val time.Duration) zap.Field {
|
||||
return zap.Int64("queueMs", val.Milliseconds())
|
||||
}
|
||||
|
||||
func TotalDur(val time.Duration) zap.Field {
|
||||
return zap.Int64("totalMs", val.Milliseconds())
|
||||
}
|
||||
|
||||
func SpaceId(val string) zap.Field {
|
||||
return zap.String("spaceId", val)
|
||||
}
|
||||
|
||||
func ObjectId(val string) zap.Field {
|
||||
return zap.String("objectId", val)
|
||||
}
|
||||
|
||||
func Identity(val string) zap.Field {
|
||||
return zap.String("identity", val)
|
||||
}
|
||||
|
||||
func IP(val string) zap.Field {
|
||||
return zap.String("ip", val)
|
||||
}
|
||||
|
||||
func (m *metric) RequestLog(ctx context.Context, fields ...zap.Field) {
|
||||
m.rpcLog.InfoCtx(ctx, "", fields...)
|
||||
}
|
|
@ -24,6 +24,7 @@ func New() Metric {
|
|||
type Metric interface {
|
||||
Registry() *prometheus.Registry
|
||||
WrapDRPCHandler(h drpc.Handler) drpc.Handler
|
||||
RequestLog(ctx context.Context, fields ...zap.Field)
|
||||
app.ComponentRunnable
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue