Fix error log when loading issues caused by a xorm bug (#7271)
* fix error log when loading issues caused by a xorm bug * upgrade packages * fix fmt * fix Consistency * fix teststokarchuk/v1.17
parent
baefea311f
commit
aa7c34cf86
@ -0,0 +1,207 @@ |
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
// +build go1.8
|
||||||
|
|
||||||
|
package mysql |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"database/sql" |
||||||
|
"database/sql/driver" |
||||||
|
) |
||||||
|
|
||||||
|
// Ping implements driver.Pinger interface
|
||||||
|
func (mc *mysqlConn) Ping(ctx context.Context) (err error) { |
||||||
|
if mc.closed.IsSet() { |
||||||
|
errLog.Print(ErrInvalidConn) |
||||||
|
return driver.ErrBadConn |
||||||
|
} |
||||||
|
|
||||||
|
if err = mc.watchCancel(ctx); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
defer mc.finish() |
||||||
|
|
||||||
|
if err = mc.writeCommandPacket(comPing); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
return mc.readResultOK() |
||||||
|
} |
||||||
|
|
||||||
|
// BeginTx implements driver.ConnBeginTx interface
|
||||||
|
func (mc *mysqlConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { |
||||||
|
if err := mc.watchCancel(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer mc.finish() |
||||||
|
|
||||||
|
if sql.IsolationLevel(opts.Isolation) != sql.LevelDefault { |
||||||
|
level, err := mapIsolationLevel(opts.Isolation) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
err = mc.exec("SET TRANSACTION ISOLATION LEVEL " + level) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return mc.begin(opts.ReadOnly) |
||||||
|
} |
||||||
|
|
||||||
|
func (mc *mysqlConn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { |
||||||
|
dargs, err := namedValueToValue(args) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if err := mc.watchCancel(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
rows, err := mc.query(query, dargs) |
||||||
|
if err != nil { |
||||||
|
mc.finish() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
rows.finish = mc.finish |
||||||
|
return rows, err |
||||||
|
} |
||||||
|
|
||||||
|
func (mc *mysqlConn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { |
||||||
|
dargs, err := namedValueToValue(args) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if err := mc.watchCancel(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer mc.finish() |
||||||
|
|
||||||
|
return mc.Exec(query, dargs) |
||||||
|
} |
||||||
|
|
||||||
|
func (mc *mysqlConn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { |
||||||
|
if err := mc.watchCancel(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
stmt, err := mc.Prepare(query) |
||||||
|
mc.finish() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
default: |
||||||
|
case <-ctx.Done(): |
||||||
|
stmt.Close() |
||||||
|
return nil, ctx.Err() |
||||||
|
} |
||||||
|
return stmt, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (stmt *mysqlStmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { |
||||||
|
dargs, err := namedValueToValue(args) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if err := stmt.mc.watchCancel(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
rows, err := stmt.query(dargs) |
||||||
|
if err != nil { |
||||||
|
stmt.mc.finish() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
rows.finish = stmt.mc.finish |
||||||
|
return rows, err |
||||||
|
} |
||||||
|
|
||||||
|
func (stmt *mysqlStmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { |
||||||
|
dargs, err := namedValueToValue(args) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if err := stmt.mc.watchCancel(ctx); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
defer stmt.mc.finish() |
||||||
|
|
||||||
|
return stmt.Exec(dargs) |
||||||
|
} |
||||||
|
|
||||||
|
func (mc *mysqlConn) watchCancel(ctx context.Context) error { |
||||||
|
if mc.watching { |
||||||
|
// Reach here if canceled,
|
||||||
|
// so the connection is already invalid
|
||||||
|
mc.cleanup() |
||||||
|
return nil |
||||||
|
} |
||||||
|
// When ctx is already cancelled, don't watch it.
|
||||||
|
if err := ctx.Err(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
// When ctx is not cancellable, don't watch it.
|
||||||
|
if ctx.Done() == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
// When watcher is not alive, can't watch it.
|
||||||
|
if mc.watcher == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
mc.watching = true |
||||||
|
mc.watcher <- ctx |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (mc *mysqlConn) startWatcher() { |
||||||
|
watcher := make(chan mysqlContext, 1) |
||||||
|
mc.watcher = watcher |
||||||
|
finished := make(chan struct{}) |
||||||
|
mc.finished = finished |
||||||
|
go func() { |
||||||
|
for { |
||||||
|
var ctx mysqlContext |
||||||
|
select { |
||||||
|
case ctx = <-watcher: |
||||||
|
case <-mc.closech: |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
case <-ctx.Done(): |
||||||
|
mc.cancel(ctx.Err()) |
||||||
|
case <-finished: |
||||||
|
case <-mc.closech: |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
func (mc *mysqlConn) CheckNamedValue(nv *driver.NamedValue) (err error) { |
||||||
|
nv.Value, err = converter{}.ConvertValue(nv.Value) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ResetSession implements driver.SessionResetter.
|
||||||
|
// (From Go 1.10)
|
||||||
|
func (mc *mysqlConn) ResetSession(ctx context.Context) error { |
||||||
|
if mc.closed.IsSet() { |
||||||
|
return driver.ErrBadConn |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
// +build go1.7
|
||||||
|
// +build !go1.8
|
||||||
|
|
||||||
|
package mysql |
||||||
|
|
||||||
|
import "crypto/tls" |
||||||
|
|
||||||
|
func cloneTLSConfig(c *tls.Config) *tls.Config { |
||||||
|
return &tls.Config{ |
||||||
|
Rand: c.Rand, |
||||||
|
Time: c.Time, |
||||||
|
Certificates: c.Certificates, |
||||||
|
NameToCertificate: c.NameToCertificate, |
||||||
|
GetCertificate: c.GetCertificate, |
||||||
|
RootCAs: c.RootCAs, |
||||||
|
NextProtos: c.NextProtos, |
||||||
|
ServerName: c.ServerName, |
||||||
|
ClientAuth: c.ClientAuth, |
||||||
|
ClientCAs: c.ClientCAs, |
||||||
|
InsecureSkipVerify: c.InsecureSkipVerify, |
||||||
|
CipherSuites: c.CipherSuites, |
||||||
|
PreferServerCipherSuites: c.PreferServerCipherSuites, |
||||||
|
SessionTicketsDisabled: c.SessionTicketsDisabled, |
||||||
|
SessionTicketKey: c.SessionTicketKey, |
||||||
|
ClientSessionCache: c.ClientSessionCache, |
||||||
|
MinVersion: c.MinVersion, |
||||||
|
MaxVersion: c.MaxVersion, |
||||||
|
CurvePreferences: c.CurvePreferences, |
||||||
|
DynamicRecordSizingDisabled: c.DynamicRecordSizingDisabled, |
||||||
|
Renegotiation: c.Renegotiation, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||||
|
//
|
||||||
|
// Copyright 2017 The Go-MySQL-Driver Authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
// +build go1.8
|
||||||
|
|
||||||
|
package mysql |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/tls" |
||||||
|
"database/sql" |
||||||
|
"database/sql/driver" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
func cloneTLSConfig(c *tls.Config) *tls.Config { |
||||||
|
return c.Clone() |
||||||
|
} |
||||||
|
|
||||||
|
func namedValueToValue(named []driver.NamedValue) ([]driver.Value, error) { |
||||||
|
dargs := make([]driver.Value, len(named)) |
||||||
|
for n, param := range named { |
||||||
|
if len(param.Name) > 0 { |
||||||
|
// TODO: support the use of Named Parameters #561
|
||||||
|
return nil, errors.New("mysql: driver does not support the use of Named Parameters") |
||||||
|
} |
||||||
|
dargs[n] = param.Value |
||||||
|
} |
||||||
|
return dargs, nil |
||||||
|
} |
||||||
|
|
||||||
|
func mapIsolationLevel(level driver.IsolationLevel) (string, error) { |
||||||
|
switch sql.IsolationLevel(level) { |
||||||
|
case sql.LevelRepeatableRead: |
||||||
|
return "REPEATABLE READ", nil |
||||||
|
case sql.LevelReadCommitted: |
||||||
|
return "READ COMMITTED", nil |
||||||
|
case sql.LevelReadUncommitted: |
||||||
|
return "READ UNCOMMITTED", nil |
||||||
|
case sql.LevelSerializable: |
||||||
|
return "SERIALIZABLE", nil |
||||||
|
default: |
||||||
|
return "", fmt.Errorf("mysql: unsupported isolation level: %v", level) |
||||||
|
} |
||||||
|
} |
@ -1 +0,0 @@ |
|||||||
module "github.com/go-xorm/builder" |
|
@ -1,15 +0,0 @@ |
|||||||
dependencies: |
|
||||||
override: |
|
||||||
# './...' is a relative pattern which means all subdirectories |
|
||||||
- go get -t -d -v ./... |
|
||||||
- go build -v |
|
||||||
|
|
||||||
database: |
|
||||||
override: |
|
||||||
- mysql -u root -e "CREATE DATABASE core_test DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci" |
|
||||||
|
|
||||||
test: |
|
||||||
override: |
|
||||||
# './...' is a relative pattern which means all subdirectories |
|
||||||
- go test -v -race |
|
||||||
- go test -v -race --dbtype=sqlite3 |
|
@ -1,401 +0,0 @@ |
|||||||
package core |
|
||||||
|
|
||||||
import ( |
|
||||||
"database/sql" |
|
||||||
"database/sql/driver" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"reflect" |
|
||||||
"regexp" |
|
||||||
"sync" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
DefaultCacheSize = 200 |
|
||||||
) |
|
||||||
|
|
||||||
func MapToSlice(query string, mp interface{}) (string, []interface{}, error) { |
|
||||||
vv := reflect.ValueOf(mp) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Map { |
|
||||||
return "", []interface{}{}, ErrNoMapPointer |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, 0, len(vv.Elem().MapKeys())) |
|
||||||
var err error |
|
||||||
query = re.ReplaceAllStringFunc(query, func(src string) string { |
|
||||||
v := vv.Elem().MapIndex(reflect.ValueOf(src[1:])) |
|
||||||
if !v.IsValid() { |
|
||||||
err = fmt.Errorf("map key %s is missing", src[1:]) |
|
||||||
} else { |
|
||||||
args = append(args, v.Interface()) |
|
||||||
} |
|
||||||
return "?" |
|
||||||
}) |
|
||||||
|
|
||||||
return query, args, err |
|
||||||
} |
|
||||||
|
|
||||||
func StructToSlice(query string, st interface{}) (string, []interface{}, error) { |
|
||||||
vv := reflect.ValueOf(st) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Struct { |
|
||||||
return "", []interface{}{}, ErrNoStructPointer |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, 0) |
|
||||||
var err error |
|
||||||
query = re.ReplaceAllStringFunc(query, func(src string) string { |
|
||||||
fv := vv.Elem().FieldByName(src[1:]).Interface() |
|
||||||
if v, ok := fv.(driver.Valuer); ok { |
|
||||||
var value driver.Value |
|
||||||
value, err = v.Value() |
|
||||||
if err != nil { |
|
||||||
return "?" |
|
||||||
} |
|
||||||
args = append(args, value) |
|
||||||
} else { |
|
||||||
args = append(args, fv) |
|
||||||
} |
|
||||||
return "?" |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
return "", []interface{}{}, err |
|
||||||
} |
|
||||||
return query, args, nil |
|
||||||
} |
|
||||||
|
|
||||||
type cacheStruct struct { |
|
||||||
value reflect.Value |
|
||||||
idx int |
|
||||||
} |
|
||||||
|
|
||||||
type DB struct { |
|
||||||
*sql.DB |
|
||||||
Mapper IMapper |
|
||||||
reflectCache map[reflect.Type]*cacheStruct |
|
||||||
reflectCacheMutex sync.RWMutex |
|
||||||
} |
|
||||||
|
|
||||||
func Open(driverName, dataSourceName string) (*DB, error) { |
|
||||||
db, err := sql.Open(driverName, dataSourceName) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &DB{ |
|
||||||
DB: db, |
|
||||||
Mapper: NewCacheMapper(&SnakeMapper{}), |
|
||||||
reflectCache: make(map[reflect.Type]*cacheStruct), |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func FromDB(db *sql.DB) *DB { |
|
||||||
return &DB{ |
|
||||||
DB: db, |
|
||||||
Mapper: NewCacheMapper(&SnakeMapper{}), |
|
||||||
reflectCache: make(map[reflect.Type]*cacheStruct), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) reflectNew(typ reflect.Type) reflect.Value { |
|
||||||
db.reflectCacheMutex.Lock() |
|
||||||
defer db.reflectCacheMutex.Unlock() |
|
||||||
cs, ok := db.reflectCache[typ] |
|
||||||
if !ok || cs.idx+1 > DefaultCacheSize-1 { |
|
||||||
cs = &cacheStruct{reflect.MakeSlice(reflect.SliceOf(typ), DefaultCacheSize, DefaultCacheSize), 0} |
|
||||||
db.reflectCache[typ] = cs |
|
||||||
} else { |
|
||||||
cs.idx = cs.idx + 1 |
|
||||||
} |
|
||||||
return cs.value.Index(cs.idx).Addr() |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) { |
|
||||||
rows, err := db.DB.Query(query, args...) |
|
||||||
if err != nil { |
|
||||||
if rows != nil { |
|
||||||
rows.Close() |
|
||||||
} |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &Rows{rows, db}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) QueryMap(query string, mp interface{}) (*Rows, error) { |
|
||||||
query, args, err := MapToSlice(query, mp) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return db.Query(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) QueryStruct(query string, st interface{}) (*Rows, error) { |
|
||||||
query, args, err := StructToSlice(query, st) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return db.Query(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) QueryRow(query string, args ...interface{}) *Row { |
|
||||||
rows, err := db.Query(query, args...) |
|
||||||
if err != nil { |
|
||||||
return &Row{nil, err} |
|
||||||
} |
|
||||||
return &Row{rows, nil} |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) QueryRowMap(query string, mp interface{}) *Row { |
|
||||||
query, args, err := MapToSlice(query, mp) |
|
||||||
if err != nil { |
|
||||||
return &Row{nil, err} |
|
||||||
} |
|
||||||
return db.QueryRow(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) QueryRowStruct(query string, st interface{}) *Row { |
|
||||||
query, args, err := StructToSlice(query, st) |
|
||||||
if err != nil { |
|
||||||
return &Row{nil, err} |
|
||||||
} |
|
||||||
return db.QueryRow(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
type Stmt struct { |
|
||||||
*sql.Stmt |
|
||||||
db *DB |
|
||||||
names map[string]int |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) Prepare(query string) (*Stmt, error) { |
|
||||||
names := make(map[string]int) |
|
||||||
var i int |
|
||||||
query = re.ReplaceAllStringFunc(query, func(src string) string { |
|
||||||
names[src[1:]] = i |
|
||||||
i += 1 |
|
||||||
return "?" |
|
||||||
}) |
|
||||||
|
|
||||||
stmt, err := db.DB.Prepare(query) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &Stmt{stmt, db, names}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) ExecMap(mp interface{}) (sql.Result, error) { |
|
||||||
vv := reflect.ValueOf(mp) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Map { |
|
||||||
return nil, errors.New("mp should be a map's pointer") |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, len(s.names)) |
|
||||||
for k, i := range s.names { |
|
||||||
args[i] = vv.Elem().MapIndex(reflect.ValueOf(k)).Interface() |
|
||||||
} |
|
||||||
return s.Stmt.Exec(args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) ExecStruct(st interface{}) (sql.Result, error) { |
|
||||||
vv := reflect.ValueOf(st) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Struct { |
|
||||||
return nil, errors.New("mp should be a map's pointer") |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, len(s.names)) |
|
||||||
for k, i := range s.names { |
|
||||||
args[i] = vv.Elem().FieldByName(k).Interface() |
|
||||||
} |
|
||||||
return s.Stmt.Exec(args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) Query(args ...interface{}) (*Rows, error) { |
|
||||||
rows, err := s.Stmt.Query(args...) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &Rows{rows, s.db}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) QueryMap(mp interface{}) (*Rows, error) { |
|
||||||
vv := reflect.ValueOf(mp) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Map { |
|
||||||
return nil, errors.New("mp should be a map's pointer") |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, len(s.names)) |
|
||||||
for k, i := range s.names { |
|
||||||
args[i] = vv.Elem().MapIndex(reflect.ValueOf(k)).Interface() |
|
||||||
} |
|
||||||
|
|
||||||
return s.Query(args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) QueryStruct(st interface{}) (*Rows, error) { |
|
||||||
vv := reflect.ValueOf(st) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Struct { |
|
||||||
return nil, errors.New("mp should be a map's pointer") |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, len(s.names)) |
|
||||||
for k, i := range s.names { |
|
||||||
args[i] = vv.Elem().FieldByName(k).Interface() |
|
||||||
} |
|
||||||
|
|
||||||
return s.Query(args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) QueryRow(args ...interface{}) *Row { |
|
||||||
rows, err := s.Query(args...) |
|
||||||
return &Row{rows, err} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) QueryRowMap(mp interface{}) *Row { |
|
||||||
vv := reflect.ValueOf(mp) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Map { |
|
||||||
return &Row{nil, errors.New("mp should be a map's pointer")} |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, len(s.names)) |
|
||||||
for k, i := range s.names { |
|
||||||
args[i] = vv.Elem().MapIndex(reflect.ValueOf(k)).Interface() |
|
||||||
} |
|
||||||
|
|
||||||
return s.QueryRow(args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Stmt) QueryRowStruct(st interface{}) *Row { |
|
||||||
vv := reflect.ValueOf(st) |
|
||||||
if vv.Kind() != reflect.Ptr || vv.Elem().Kind() != reflect.Struct { |
|
||||||
return &Row{nil, errors.New("st should be a struct's pointer")} |
|
||||||
} |
|
||||||
|
|
||||||
args := make([]interface{}, len(s.names)) |
|
||||||
for k, i := range s.names { |
|
||||||
args[i] = vv.Elem().FieldByName(k).Interface() |
|
||||||
} |
|
||||||
|
|
||||||
return s.QueryRow(args...) |
|
||||||
} |
|
||||||
|
|
||||||
var ( |
|
||||||
re = regexp.MustCompile(`[?](\w+)`) |
|
||||||
) |
|
||||||
|
|
||||||
// insert into (name) values (?)
|
|
||||||
// insert into (name) values (?name)
|
|
||||||
func (db *DB) ExecMap(query string, mp interface{}) (sql.Result, error) { |
|
||||||
query, args, err := MapToSlice(query, mp) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return db.DB.Exec(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) ExecStruct(query string, st interface{}) (sql.Result, error) { |
|
||||||
query, args, err := StructToSlice(query, st) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return db.DB.Exec(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
type EmptyScanner struct { |
|
||||||
} |
|
||||||
|
|
||||||
func (EmptyScanner) Scan(src interface{}) error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
type Tx struct { |
|
||||||
*sql.Tx |
|
||||||
db *DB |
|
||||||
} |
|
||||||
|
|
||||||
func (db *DB) Begin() (*Tx, error) { |
|
||||||
tx, err := db.DB.Begin() |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &Tx{tx, db}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) Prepare(query string) (*Stmt, error) { |
|
||||||
names := make(map[string]int) |
|
||||||
var i int |
|
||||||
query = re.ReplaceAllStringFunc(query, func(src string) string { |
|
||||||
names[src[1:]] = i |
|
||||||
i += 1 |
|
||||||
return "?" |
|
||||||
}) |
|
||||||
|
|
||||||
stmt, err := tx.Tx.Prepare(query) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &Stmt{stmt, tx.db, names}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) Stmt(stmt *Stmt) *Stmt { |
|
||||||
// TODO:
|
|
||||||
return stmt |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) ExecMap(query string, mp interface{}) (sql.Result, error) { |
|
||||||
query, args, err := MapToSlice(query, mp) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return tx.Tx.Exec(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) ExecStruct(query string, st interface{}) (sql.Result, error) { |
|
||||||
query, args, err := StructToSlice(query, st) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return tx.Tx.Exec(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) { |
|
||||||
rows, err := tx.Tx.Query(query, args...) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return &Rows{rows, tx.db}, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) QueryMap(query string, mp interface{}) (*Rows, error) { |
|
||||||
query, args, err := MapToSlice(query, mp) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return tx.Query(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) QueryStruct(query string, st interface{}) (*Rows, error) { |
|
||||||
query, args, err := StructToSlice(query, st) |
|
||||||
if err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
return tx.Query(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) QueryRow(query string, args ...interface{}) *Row { |
|
||||||
rows, err := tx.Query(query, args...) |
|
||||||
return &Row{rows, err} |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) QueryRowMap(query string, mp interface{}) *Row { |
|
||||||
query, args, err := MapToSlice(query, mp) |
|
||||||
if err != nil { |
|
||||||
return &Row{nil, err} |
|
||||||
} |
|
||||||
return tx.QueryRow(query, args...) |
|
||||||
} |
|
||||||
|
|
||||||
func (tx *Tx) QueryRowStruct(query string, st interface{}) *Row { |
|
||||||
query, args, err := StructToSlice(query, st) |
|
||||||
if err != nil { |
|
||||||
return &Row{nil, err} |
|
||||||
} |
|
||||||
return tx.QueryRow(query, args...) |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
module "github.com/go-xorm/core" |
|
@ -1,41 +0,0 @@ |
|||||||
dependencies: |
|
||||||
override: |
|
||||||
# './...' is a relative pattern which means all subdirectories |
|
||||||
- go get -t -d -v ./... |
|
||||||
- go get -t -d -v github.com/go-xorm/tests |
|
||||||
- go get -u github.com/go-xorm/core |
|
||||||
- go get -u github.com/go-xorm/builder |
|
||||||
- go build -v |
|
||||||
|
|
||||||
database: |
|
||||||
override: |
|
||||||
- mysql -u root -e "CREATE DATABASE xorm_test DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci" |
|
||||||
- mysql -u root -e "CREATE DATABASE xorm_test1 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci" |
|
||||||
- mysql -u root -e "CREATE DATABASE xorm_test2 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci" |
|
||||||
- mysql -u root -e "CREATE DATABASE xorm_test3 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci" |
|
||||||
- createdb -p 5432 -e -U postgres xorm_test |
|
||||||
- createdb -p 5432 -e -U postgres xorm_test1 |
|
||||||
- createdb -p 5432 -e -U postgres xorm_test2 |
|
||||||
- createdb -p 5432 -e -U postgres xorm_test3 |
|
||||||
- psql xorm_test postgres -c "create schema xorm" |
|
||||||
|
|
||||||
test: |
|
||||||
override: |
|
||||||
# './...' is a relative pattern which means all subdirectories |
|
||||||
- go get -u github.com/wadey/gocovmerge |
|
||||||
- go test -v -race -db="sqlite3" -conn_str="./test.db" -coverprofile=coverage1-1.txt -covermode=atomic |
|
||||||
- go test -v -race -db="sqlite3" -conn_str="./test.db" -cache=true -coverprofile=coverage1-2.txt -covermode=atomic |
|
||||||
- go test -v -race -db="mysql" -conn_str="root:@/xorm_test" -coverprofile=coverage2-1.txt -covermode=atomic |
|
||||||
- go test -v -race -db="mysql" -conn_str="root:@/xorm_test" -cache=true -coverprofile=coverage2-2.txt -covermode=atomic |
|
||||||
- go test -v -race -db="mymysql" -conn_str="xorm_test/root/" -coverprofile=coverage3-1.txt -covermode=atomic |
|
||||||
- go test -v -race -db="mymysql" -conn_str="xorm_test/root/" -cache=true -coverprofile=coverage3-2.txt -covermode=atomic |
|
||||||
- go test -v -race -db="postgres" -conn_str="dbname=xorm_test sslmode=disable" -coverprofile=coverage4-1.txt -covermode=atomic |
|
||||||
- go test -v -race -db="postgres" -conn_str="dbname=xorm_test sslmode=disable" -cache=true -coverprofile=coverage4-2.txt -covermode=atomic |
|
||||||
- go test -v -race -db="postgres" -conn_str="dbname=xorm_test sslmode=disable" -schema=xorm -coverprofile=coverage5-1.txt -covermode=atomic |
|
||||||
- go test -v -race -db="postgres" -conn_str="dbname=xorm_test sslmode=disable" -schema=xorm -cache=true -coverprofile=coverage5-2.txt -covermode=atomic |
|
||||||
- gocovmerge coverage1-1.txt coverage1-2.txt coverage2-1.txt coverage2-2.txt coverage3-1.txt coverage3-2.txt coverage4-1.txt coverage4-2.txt coverage5-1.txt coverage5-2.txt > coverage.txt |
|
||||||
- cd /home/ubuntu/.go_workspace/src/github.com/go-xorm/tests && ./sqlite3.sh |
|
||||||
- cd /home/ubuntu/.go_workspace/src/github.com/go-xorm/tests && ./mysql.sh |
|
||||||
- cd /home/ubuntu/.go_workspace/src/github.com/go-xorm/tests && ./postgres.sh |
|
||||||
post: |
|
||||||
- bash <(curl -s https://codecov.io/bash) |
|
@ -0,0 +1,28 @@ |
|||||||
|
// Copyright 2019 The Xorm Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build go1.8
|
||||||
|
|
||||||
|
package xorm |
||||||
|
|
||||||
|
import "context" |
||||||
|
|
||||||
|
// Context creates a session with the context
|
||||||
|
func (engine *Engine) Context(ctx context.Context) *Session { |
||||||
|
session := engine.NewSession() |
||||||
|
session.isAutoClose = true |
||||||
|
return session.Context(ctx) |
||||||
|
} |
||||||
|
|
||||||
|
// SetDefaultContext set the default context
|
||||||
|
func (engine *Engine) SetDefaultContext(ctx context.Context) { |
||||||
|
engine.defaultContext = ctx |
||||||
|
} |
||||||
|
|
||||||
|
// PingContext tests if database is alive
|
||||||
|
func (engine *Engine) PingContext(ctx context.Context) error { |
||||||
|
session := engine.NewSession() |
||||||
|
defer session.Close() |
||||||
|
return session.PingContext(ctx) |
||||||
|
} |
@ -1,24 +1,24 @@ |
|||||||
module github.com/go-xorm/xorm |
module github.com/go-xorm/xorm |
||||||
|
|
||||||
require ( |
require ( |
||||||
|
cloud.google.com/go v0.34.0 // indirect |
||||||
github.com/cockroachdb/apd v1.1.0 // indirect |
github.com/cockroachdb/apd v1.1.0 // indirect |
||||||
github.com/davecgh/go-spew v1.1.1 // indirect |
github.com/davecgh/go-spew v1.1.1 // indirect |
||||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f |
github.com/denisenkom/go-mssqldb v0.0.0-20190121005146-b04fd42d9952 |
||||||
github.com/go-sql-driver/mysql v1.4.0 |
github.com/go-sql-driver/mysql v1.4.1 |
||||||
github.com/go-xorm/builder v0.3.2 |
github.com/google/go-cmp v0.2.0 // indirect |
||||||
github.com/go-xorm/core v0.6.0 |
|
||||||
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a // indirect |
|
||||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect |
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect |
||||||
github.com/jackc/pgx v3.2.0+incompatible |
github.com/jackc/pgx v3.3.0+incompatible |
||||||
github.com/kr/pretty v0.1.0 // indirect |
github.com/kr/pretty v0.1.0 // indirect |
||||||
github.com/lib/pq v1.0.0 |
github.com/lib/pq v1.0.0 |
||||||
github.com/mattn/go-sqlite3 v1.9.0 |
github.com/mattn/go-sqlite3 v1.10.0 |
||||||
github.com/pkg/errors v0.8.0 // indirect |
github.com/pkg/errors v0.8.1 // indirect |
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect |
|
||||||
github.com/satori/go.uuid v1.2.0 // indirect |
github.com/satori/go.uuid v1.2.0 // indirect |
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect |
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect |
||||||
github.com/stretchr/testify v1.2.2 |
github.com/stretchr/testify v1.3.0 |
||||||
github.com/ziutek/mymysql v1.5.4 |
github.com/ziutek/mymysql v1.5.4 |
||||||
|
golang.org/x/crypto v0.0.0-20190122013713-64072686203f // indirect |
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect |
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect |
||||||
gopkg.in/stretchr/testify.v1 v1.2.2 |
xorm.io/builder v0.3.5 |
||||||
|
xorm.io/core v0.6.3 |
||||||
) |
) |
||||||
|
@ -0,0 +1,31 @@ |
|||||||
|
// Copyright 2019 The Xorm Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package xorm |
||||||
|
|
||||||
|
import "encoding/json" |
||||||
|
|
||||||
|
// JSONInterface represents an interface to handle json data
|
||||||
|
type JSONInterface interface { |
||||||
|
Marshal(v interface{}) ([]byte, error) |
||||||
|
Unmarshal(data []byte, v interface{}) error |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
// DefaultJSONHandler default json handler
|
||||||
|
DefaultJSONHandler JSONInterface = StdJSON{} |
||||||
|
) |
||||||
|
|
||||||
|
// StdJSON implements JSONInterface via encoding/json
|
||||||
|
type StdJSON struct{} |
||||||
|
|
||||||
|
// Marshal implements JSONInterface
|
||||||
|
func (StdJSON) Marshal(v interface{}) ([]byte, error) { |
||||||
|
return json.Marshal(v) |
||||||
|
} |
||||||
|
|
||||||
|
// Unmarshal implements JSONInterface
|
||||||
|
func (StdJSON) Unmarshal(data []byte, v interface{}) error { |
||||||
|
return json.Unmarshal(data, v) |
||||||
|
} |
@ -1,18 +1,15 @@ |
|||||||
// Copyright 2017 The Xorm Authors. All rights reserved.
|
// Copyright 2019 The Xorm Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// +build go1.8
|
|
||||||
|
|
||||||
package xorm |
package xorm |
||||||
|
|
||||||
import "context" |
import "context" |
||||||
|
|
||||||
// PingContext tests if database is alive
|
// Context sets the context on this session
|
||||||
func (engine *Engine) PingContext(ctx context.Context) error { |
func (session *Session) Context(ctx context.Context) *Session { |
||||||
session := engine.NewSession() |
session.ctx = ctx |
||||||
defer session.Close() |
return session |
||||||
return session.PingContext(ctx) |
|
||||||
} |
} |
||||||
|
|
||||||
// PingContext test if database is ok
|
// PingContext test if database is ok
|
@ -1 +1 @@ |
|||||||
go test -db=mssql -conn_str="server=192.168.1.58;user id=sa;password=123456;database=xorm_test" |
go test -db=mssql -conn_str="server=localhost;user id=sa;password=yourStrong(!)Password;database=xorm_test" |
@ -0,0 +1 @@ |
|||||||
|
go test -db=mysql -conn_str="root:@tcp(localhost:4000)/xorm_test" -ignore_select_update=true |
@ -1,682 +0,0 @@ |
|||||||
// Copyright 2011 Google Inc. All rights reserved.
|
|
||||||
// Use of this source code is governed by the Apache 2.0
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !appengine
|
|
||||||
// +build !go1.7
|
|
||||||
|
|
||||||
package internal |
|
||||||
|
|
||||||
import ( |
|
||||||
"bytes" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"io/ioutil" |
|
||||||
"log" |
|
||||||
"net" |
|
||||||
"net/http" |
|
||||||
"net/url" |
|
||||||
"os" |
|
||||||
"runtime" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
"sync" |
|
||||||
"sync/atomic" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/golang/protobuf/proto" |
|
||||||
netcontext "golang.org/x/net/context" |
|
||||||
|
|
||||||
basepb "google.golang.org/appengine/internal/base" |
|
||||||
logpb "google.golang.org/appengine/internal/log" |
|
||||||
remotepb "google.golang.org/appengine/internal/remote_api" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
apiPath = "/rpc_http" |
|
||||||
defaultTicketSuffix = "/default.20150612t184001.0" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
// Incoming headers.
|
|
||||||
ticketHeader = http.CanonicalHeaderKey("X-AppEngine-API-Ticket") |
|
||||||
dapperHeader = http.CanonicalHeaderKey("X-Google-DapperTraceInfo") |
|
||||||
traceHeader = http.CanonicalHeaderKey("X-Cloud-Trace-Context") |
|
||||||
curNamespaceHeader = http.CanonicalHeaderKey("X-AppEngine-Current-Namespace") |
|
||||||
userIPHeader = http.CanonicalHeaderKey("X-AppEngine-User-IP") |
|
||||||
remoteAddrHeader = http.CanonicalHeaderKey("X-AppEngine-Remote-Addr") |
|
||||||
|
|
||||||
// Outgoing headers.
|
|
||||||
apiEndpointHeader = http.CanonicalHeaderKey("X-Google-RPC-Service-Endpoint") |
|
||||||
apiEndpointHeaderValue = []string{"app-engine-apis"} |
|
||||||
apiMethodHeader = http.CanonicalHeaderKey("X-Google-RPC-Service-Method") |
|
||||||
apiMethodHeaderValue = []string{"/VMRemoteAPI.CallRemoteAPI"} |
|
||||||
apiDeadlineHeader = http.CanonicalHeaderKey("X-Google-RPC-Service-Deadline") |
|
||||||
apiContentType = http.CanonicalHeaderKey("Content-Type") |
|
||||||
apiContentTypeValue = []string{"application/octet-stream"} |
|
||||||
logFlushHeader = http.CanonicalHeaderKey("X-AppEngine-Log-Flush-Count") |
|
||||||
|
|
||||||
apiHTTPClient = &http.Client{ |
|
||||||
Transport: &http.Transport{ |
|
||||||
Proxy: http.ProxyFromEnvironment, |
|
||||||
Dial: limitDial, |
|
||||||
}, |
|
||||||
} |
|
||||||
|
|
||||||
defaultTicketOnce sync.Once |
|
||||||
defaultTicket string |
|
||||||
) |
|
||||||
|
|
||||||
func apiURL() *url.URL { |
|
||||||
host, port := "appengine.googleapis.internal", "10001" |
|
||||||
if h := os.Getenv("API_HOST"); h != "" { |
|
||||||
host = h |
|
||||||
} |
|
||||||
if p := os.Getenv("API_PORT"); p != "" { |
|
||||||
port = p |
|
||||||
} |
|
||||||
return &url.URL{ |
|
||||||
Scheme: "http", |
|
||||||
Host: host + ":" + port, |
|
||||||
Path: apiPath, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func handleHTTP(w http.ResponseWriter, r *http.Request) { |
|
||||||
c := &context{ |
|
||||||
req: r, |
|
||||||
outHeader: w.Header(), |
|
||||||
apiURL: apiURL(), |
|
||||||
} |
|
||||||
stopFlushing := make(chan int) |
|
||||||
|
|
||||||
ctxs.Lock() |
|
||||||
ctxs.m[r] = c |
|
||||||
ctxs.Unlock() |
|
||||||
defer func() { |
|
||||||
ctxs.Lock() |
|
||||||
delete(ctxs.m, r) |
|
||||||
ctxs.Unlock() |
|
||||||
}() |
|
||||||
|
|
||||||
// Patch up RemoteAddr so it looks reasonable.
|
|
||||||
if addr := r.Header.Get(userIPHeader); addr != "" { |
|
||||||
r.RemoteAddr = addr |
|
||||||
} else if addr = r.Header.Get(remoteAddrHeader); addr != "" { |
|
||||||
r.RemoteAddr = addr |
|
||||||
} else { |
|
||||||
// Should not normally reach here, but pick a sensible default anyway.
|
|
||||||
r.RemoteAddr = "127.0.0.1" |
|
||||||
} |
|
||||||
// The address in the headers will most likely be of these forms:
|
|
||||||
// 123.123.123.123
|
|
||||||
// 2001:db8::1
|
|
||||||
// net/http.Request.RemoteAddr is specified to be in "IP:port" form.
|
|
||||||
if _, _, err := net.SplitHostPort(r.RemoteAddr); err != nil { |
|
||||||
// Assume the remote address is only a host; add a default port.
|
|
||||||
r.RemoteAddr = net.JoinHostPort(r.RemoteAddr, "80") |
|
||||||
} |
|
||||||
|
|
||||||
// Start goroutine responsible for flushing app logs.
|
|
||||||
// This is done after adding c to ctx.m (and stopped before removing it)
|
|
||||||
// because flushing logs requires making an API call.
|
|
||||||
go c.logFlusher(stopFlushing) |
|
||||||
|
|
||||||
executeRequestSafely(c, r) |
|
||||||
c.outHeader = nil // make sure header changes aren't respected any more
|
|
||||||
|
|
||||||
stopFlushing <- 1 // any logging beyond this point will be dropped
|
|
||||||
|
|
||||||
// Flush any pending logs asynchronously.
|
|
||||||
c.pendingLogs.Lock() |
|
||||||
flushes := c.pendingLogs.flushes |
|
||||||
if len(c.pendingLogs.lines) > 0 { |
|
||||||
flushes++ |
|
||||||
} |
|
||||||
c.pendingLogs.Unlock() |
|
||||||
go c.flushLog(false) |
|
||||||
w.Header().Set(logFlushHeader, strconv.Itoa(flushes)) |
|
||||||
|
|
||||||
// Avoid nil Write call if c.Write is never called.
|
|
||||||
if c.outCode != 0 { |
|
||||||
w.WriteHeader(c.outCode) |
|
||||||
} |
|
||||||
if c.outBody != nil { |
|
||||||
w.Write(c.outBody) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func executeRequestSafely(c *context, r *http.Request) { |
|
||||||
defer func() { |
|
||||||
if x := recover(); x != nil { |
|
||||||
logf(c, 4, "%s", renderPanic(x)) // 4 == critical
|
|
||||||
c.outCode = 500 |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
http.DefaultServeMux.ServeHTTP(c, r) |
|
||||||
} |
|
||||||
|
|
||||||
func renderPanic(x interface{}) string { |
|
||||||
buf := make([]byte, 16<<10) // 16 KB should be plenty
|
|
||||||
buf = buf[:runtime.Stack(buf, false)] |
|
||||||
|
|
||||||
// Remove the first few stack frames:
|
|
||||||
// this func
|
|
||||||
// the recover closure in the caller
|
|
||||||
// That will root the stack trace at the site of the panic.
|
|
||||||
const ( |
|
||||||
skipStart = "internal.renderPanic" |
|
||||||
skipFrames = 2 |
|
||||||
) |
|
||||||
start := bytes.Index(buf, []byte(skipStart)) |
|
||||||
p := start |
|
||||||
for i := 0; i < skipFrames*2 && p+1 < len(buf); i++ { |
|
||||||
p = bytes.IndexByte(buf[p+1:], '\n') + p + 1 |
|
||||||
if p < 0 { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
if p >= 0 { |
|
||||||
// buf[start:p+1] is the block to remove.
|
|
||||||
// Copy buf[p+1:] over buf[start:] and shrink buf.
|
|
||||||
copy(buf[start:], buf[p+1:]) |
|
||||||
buf = buf[:len(buf)-(p+1-start)] |
|
||||||
} |
|
||||||
|
|
||||||
// Add panic heading.
|
|
||||||
head := fmt.Sprintf("panic: %v\n\n", x) |
|
||||||
if len(head) > len(buf) { |
|
||||||
// Extremely unlikely to happen.
|
|
||||||
return head |
|
||||||
} |
|
||||||
copy(buf[len(head):], buf) |
|
||||||
copy(buf, head) |
|
||||||
|
|
||||||
return string(buf) |
|
||||||
} |
|
||||||
|
|
||||||
var ctxs = struct { |
|
||||||
sync.Mutex |
|
||||||
m map[*http.Request]*context |
|
||||||
bg *context // background context, lazily initialized
|
|
||||||
// dec is used by tests to decorate the netcontext.Context returned
|
|
||||||
// for a given request. This allows tests to add overrides (such as
|
|
||||||
// WithAppIDOverride) to the context. The map is nil outside tests.
|
|
||||||
dec map[*http.Request]func(netcontext.Context) netcontext.Context |
|
||||||
}{ |
|
||||||
m: make(map[*http.Request]*context), |
|
||||||
} |
|
||||||
|
|
||||||
// context represents the context of an in-flight HTTP request.
|
|
||||||
// It implements the appengine.Context and http.ResponseWriter interfaces.
|
|
||||||
type context struct { |
|
||||||
req *http.Request |
|
||||||
|
|
||||||
outCode int |
|
||||||
outHeader http.Header |
|
||||||
outBody []byte |
|
||||||
|
|
||||||
pendingLogs struct { |
|
||||||
sync.Mutex |
|
||||||
lines []*logpb.UserAppLogLine |
|
||||||
flushes int |
|
||||||
} |
|
||||||
|
|
||||||
apiURL *url.URL |
|
||||||
} |
|
||||||
|
|
||||||
var contextKey = "holds a *context" |
|
||||||
|
|
||||||
// fromContext returns the App Engine context or nil if ctx is not
|
|
||||||
// derived from an App Engine context.
|
|
||||||
func fromContext(ctx netcontext.Context) *context { |
|
||||||
c, _ := ctx.Value(&contextKey).(*context) |
|
||||||
return c |
|
||||||
} |
|
||||||
|
|
||||||
func withContext(parent netcontext.Context, c *context) netcontext.Context { |
|
||||||
ctx := netcontext.WithValue(parent, &contextKey, c) |
|
||||||
if ns := c.req.Header.Get(curNamespaceHeader); ns != "" { |
|
||||||
ctx = withNamespace(ctx, ns) |
|
||||||
} |
|
||||||
return ctx |
|
||||||
} |
|
||||||
|
|
||||||
func toContext(c *context) netcontext.Context { |
|
||||||
return withContext(netcontext.Background(), c) |
|
||||||
} |
|
||||||
|
|
||||||
func IncomingHeaders(ctx netcontext.Context) http.Header { |
|
||||||
if c := fromContext(ctx); c != nil { |
|
||||||
return c.req.Header |
|
||||||
} |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func ReqContext(req *http.Request) netcontext.Context { |
|
||||||
return WithContext(netcontext.Background(), req) |
|
||||||
} |
|
||||||
|
|
||||||
func WithContext(parent netcontext.Context, req *http.Request) netcontext.Context { |
|
||||||
ctxs.Lock() |
|
||||||
c := ctxs.m[req] |
|
||||||
d := ctxs.dec[req] |
|
||||||
ctxs.Unlock() |
|
||||||
|
|
||||||
if d != nil { |
|
||||||
parent = d(parent) |
|
||||||
} |
|
||||||
|
|
||||||
if c == nil { |
|
||||||
// Someone passed in an http.Request that is not in-flight.
|
|
||||||
// We panic here rather than panicking at a later point
|
|
||||||
// so that stack traces will be more sensible.
|
|
||||||
log.Panic("appengine: NewContext passed an unknown http.Request") |
|
||||||
} |
|
||||||
return withContext(parent, c) |
|
||||||
} |
|
||||||
|
|
||||||
// DefaultTicket returns a ticket used for background context or dev_appserver.
|
|
||||||
func DefaultTicket() string { |
|
||||||
defaultTicketOnce.Do(func() { |
|
||||||
if IsDevAppServer() { |
|
||||||
defaultTicket = "testapp" + defaultTicketSuffix |
|
||||||
return |
|
||||||
} |
|
||||||
appID := partitionlessAppID() |
|
||||||
escAppID := strings.Replace(strings.Replace(appID, ":", "_", -1), ".", "_", -1) |
|
||||||
majVersion := VersionID(nil) |
|
||||||
if i := strings.Index(majVersion, "."); i > 0 { |
|
||||||
majVersion = majVersion[:i] |
|
||||||
} |
|
||||||
defaultTicket = fmt.Sprintf("%s/%s.%s.%s", escAppID, ModuleName(nil), majVersion, InstanceID()) |
|
||||||
}) |
|
||||||
return defaultTicket |
|
||||||
} |
|
||||||
|
|
||||||
func BackgroundContext() netcontext.Context { |
|
||||||
ctxs.Lock() |
|
||||||
defer ctxs.Unlock() |
|
||||||
|
|
||||||
if ctxs.bg != nil { |
|
||||||
return toContext(ctxs.bg) |
|
||||||
} |
|
||||||
|
|
||||||
// Compute background security ticket.
|
|
||||||
ticket := DefaultTicket() |
|
||||||
|
|
||||||
ctxs.bg = &context{ |
|
||||||
req: &http.Request{ |
|
||||||
Header: http.Header{ |
|
||||||
ticketHeader: []string{ticket}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
apiURL: apiURL(), |
|
||||||
} |
|
||||||
|
|
||||||
// TODO(dsymonds): Wire up the shutdown handler to do a final flush.
|
|
||||||
go ctxs.bg.logFlusher(make(chan int)) |
|
||||||
|
|
||||||
return toContext(ctxs.bg) |
|
||||||
} |
|
||||||
|
|
||||||
// RegisterTestRequest registers the HTTP request req for testing, such that
|
|
||||||
// any API calls are sent to the provided URL. It returns a closure to delete
|
|
||||||
// the registration.
|
|
||||||
// It should only be used by aetest package.
|
|
||||||
func RegisterTestRequest(req *http.Request, apiURL *url.URL, decorate func(netcontext.Context) netcontext.Context) (*http.Request, func()) { |
|
||||||
c := &context{ |
|
||||||
req: req, |
|
||||||
apiURL: apiURL, |
|
||||||
} |
|
||||||
ctxs.Lock() |
|
||||||
defer ctxs.Unlock() |
|
||||||
if _, ok := ctxs.m[req]; ok { |
|
||||||
log.Panic("req already associated with context") |
|
||||||
} |
|
||||||
if _, ok := ctxs.dec[req]; ok { |
|
||||||
log.Panic("req already associated with context") |
|
||||||
} |
|
||||||
if ctxs.dec == nil { |
|
||||||
ctxs.dec = make(map[*http.Request]func(netcontext.Context) netcontext.Context) |
|
||||||
} |
|
||||||
ctxs.m[req] = c |
|
||||||
ctxs.dec[req] = decorate |
|
||||||
|
|
||||||
return req, func() { |
|
||||||
ctxs.Lock() |
|
||||||
delete(ctxs.m, req) |
|
||||||
delete(ctxs.dec, req) |
|
||||||
ctxs.Unlock() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
var errTimeout = &CallError{ |
|
||||||
Detail: "Deadline exceeded", |
|
||||||
Code: int32(remotepb.RpcError_CANCELLED), |
|
||||||
Timeout: true, |
|
||||||
} |
|
||||||
|
|
||||||
func (c *context) Header() http.Header { return c.outHeader } |
|
||||||
|
|
||||||
// Copied from $GOROOT/src/pkg/net/http/transfer.go. Some response status
|
|
||||||
// codes do not permit a response body (nor response entity headers such as
|
|
||||||
// Content-Length, Content-Type, etc).
|
|
||||||
func bodyAllowedForStatus(status int) bool { |
|
||||||
switch { |
|
||||||
case status >= 100 && status <= 199: |
|
||||||
return false |
|
||||||
case status == 204: |
|
||||||
return false |
|
||||||
case status == 304: |
|
||||||
return false |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
func (c *context) Write(b []byte) (int, error) { |
|
||||||
if c.outCode == 0 { |
|
||||||
c.WriteHeader(http.StatusOK) |
|
||||||
} |
|
||||||
if len(b) > 0 && !bodyAllowedForStatus(c.outCode) { |
|
||||||
return 0, http.ErrBodyNotAllowed |
|
||||||
} |
|
||||||
c.outBody = append(c.outBody, b...) |
|
||||||
return len(b), nil |
|
||||||
} |
|
||||||
|
|
||||||
func (c *context) WriteHeader(code int) { |
|
||||||
if c.outCode != 0 { |
|
||||||
logf(c, 3, "WriteHeader called multiple times on request.") // error level
|
|
||||||
return |
|
||||||
} |
|
||||||
c.outCode = code |
|
||||||
} |
|
||||||
|
|
||||||
func (c *context) post(body []byte, timeout time.Duration) (b []byte, err error) { |
|
||||||
hreq := &http.Request{ |
|
||||||
Method: "POST", |
|
||||||
URL: c.apiURL, |
|
||||||
Header: http.Header{ |
|
||||||
apiEndpointHeader: apiEndpointHeaderValue, |
|
||||||
apiMethodHeader: apiMethodHeaderValue, |
|
||||||
apiContentType: apiContentTypeValue, |
|
||||||
apiDeadlineHeader: []string{strconv.FormatFloat(timeout.Seconds(), 'f', -1, 64)}, |
|
||||||
}, |
|
||||||
Body: ioutil.NopCloser(bytes.NewReader(body)), |
|
||||||
ContentLength: int64(len(body)), |
|
||||||
Host: c.apiURL.Host, |
|
||||||
} |
|
||||||
if info := c.req.Header.Get(dapperHeader); info != "" { |
|
||||||
hreq.Header.Set(dapperHeader, info) |
|
||||||
} |
|
||||||
if info := c.req.Header.Get(traceHeader); info != "" { |
|
||||||
hreq.Header.Set(traceHeader, info) |
|
||||||
} |
|
||||||
|
|
||||||
tr := apiHTTPClient.Transport.(*http.Transport) |
|
||||||
|
|
||||||
var timedOut int32 // atomic; set to 1 if timed out
|
|
||||||
t := time.AfterFunc(timeout, func() { |
|
||||||
atomic.StoreInt32(&timedOut, 1) |
|
||||||
tr.CancelRequest(hreq) |
|
||||||
}) |
|
||||||
defer t.Stop() |
|
||||||
defer func() { |
|
||||||
// Check if timeout was exceeded.
|
|
||||||
if atomic.LoadInt32(&timedOut) != 0 { |
|
||||||
err = errTimeout |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
hresp, err := apiHTTPClient.Do(hreq) |
|
||||||
if err != nil { |
|
||||||
return nil, &CallError{ |
|
||||||
Detail: fmt.Sprintf("service bridge HTTP failed: %v", err), |
|
||||||
Code: int32(remotepb.RpcError_UNKNOWN), |
|
||||||
} |
|
||||||
} |
|
||||||
defer hresp.Body.Close() |
|
||||||
hrespBody, err := ioutil.ReadAll(hresp.Body) |
|
||||||
if hresp.StatusCode != 200 { |
|
||||||
return nil, &CallError{ |
|
||||||
Detail: fmt.Sprintf("service bridge returned HTTP %d (%q)", hresp.StatusCode, hrespBody), |
|
||||||
Code: int32(remotepb.RpcError_UNKNOWN), |
|
||||||
} |
|
||||||
} |
|
||||||
if err != nil { |
|
||||||
return nil, &CallError{ |
|
||||||
Detail: fmt.Sprintf("service bridge response bad: %v", err), |
|
||||||
Code: int32(remotepb.RpcError_UNKNOWN), |
|
||||||
} |
|
||||||
} |
|
||||||
return hrespBody, nil |
|
||||||
} |
|
||||||
|
|
||||||
func Call(ctx netcontext.Context, service, method string, in, out proto.Message) error { |
|
||||||
if ns := NamespaceFromContext(ctx); ns != "" { |
|
||||||
if fn, ok := NamespaceMods[service]; ok { |
|
||||||
fn(in, ns) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if f, ctx, ok := callOverrideFromContext(ctx); ok { |
|
||||||
return f(ctx, service, method, in, out) |
|
||||||
} |
|
||||||
|
|
||||||
// Handle already-done contexts quickly.
|
|
||||||
select { |
|
||||||
case <-ctx.Done(): |
|
||||||
return ctx.Err() |
|
||||||
default: |
|
||||||
} |
|
||||||
|
|
||||||
c := fromContext(ctx) |
|
||||||
if c == nil { |
|
||||||
// Give a good error message rather than a panic lower down.
|
|
||||||
return errNotAppEngineContext |
|
||||||
} |
|
||||||
|
|
||||||
// Apply transaction modifications if we're in a transaction.
|
|
||||||
if t := transactionFromContext(ctx); t != nil { |
|
||||||
if t.finished { |
|
||||||
return errors.New("transaction context has expired") |
|
||||||
} |
|
||||||
applyTransaction(in, &t.transaction) |
|
||||||
} |
|
||||||
|
|
||||||
// Default RPC timeout is 60s.
|
|
||||||
timeout := 60 * time.Second |
|
||||||
if deadline, ok := ctx.Deadline(); ok { |
|
||||||
timeout = deadline.Sub(time.Now()) |
|
||||||
} |
|
||||||
|
|
||||||
data, err := proto.Marshal(in) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
ticket := c.req.Header.Get(ticketHeader) |
|
||||||
// Use a test ticket under test environment.
|
|
||||||
if ticket == "" { |
|
||||||
if appid := ctx.Value(&appIDOverrideKey); appid != nil { |
|
||||||
ticket = appid.(string) + defaultTicketSuffix |
|
||||||
} |
|
||||||
} |
|
||||||
// Fall back to use background ticket when the request ticket is not available in Flex or dev_appserver.
|
|
||||||
if ticket == "" { |
|
||||||
ticket = DefaultTicket() |
|
||||||
} |
|
||||||
req := &remotepb.Request{ |
|
||||||
ServiceName: &service, |
|
||||||
Method: &method, |
|
||||||
Request: data, |
|
||||||
RequestId: &ticket, |
|
||||||
} |
|
||||||
hreqBody, err := proto.Marshal(req) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
hrespBody, err := c.post(hreqBody, timeout) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
res := &remotepb.Response{} |
|
||||||
if err := proto.Unmarshal(hrespBody, res); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if res.RpcError != nil { |
|
||||||
ce := &CallError{ |
|
||||||
Detail: res.RpcError.GetDetail(), |
|
||||||
Code: *res.RpcError.Code, |
|
||||||
} |
|
||||||
switch remotepb.RpcError_ErrorCode(ce.Code) { |
|
||||||
case remotepb.RpcError_CANCELLED, remotepb.RpcError_DEADLINE_EXCEEDED: |
|
||||||
ce.Timeout = true |
|
||||||
} |
|
||||||
return ce |
|
||||||
} |
|
||||||
if res.ApplicationError != nil { |
|
||||||
return &APIError{ |
|
||||||
Service: *req.ServiceName, |
|
||||||
Detail: res.ApplicationError.GetDetail(), |
|
||||||
Code: *res.ApplicationError.Code, |
|
||||||
} |
|
||||||
} |
|
||||||
if res.Exception != nil || res.JavaException != nil { |
|
||||||
// This shouldn't happen, but let's be defensive.
|
|
||||||
return &CallError{ |
|
||||||
Detail: "service bridge returned exception", |
|
||||||
Code: int32(remotepb.RpcError_UNKNOWN), |
|
||||||
} |
|
||||||
} |
|
||||||
return proto.Unmarshal(res.Response, out) |
|
||||||
} |
|
||||||
|
|
||||||
func (c *context) Request() *http.Request { |
|
||||||
return c.req |
|
||||||
} |
|
||||||
|
|
||||||
func (c *context) addLogLine(ll *logpb.UserAppLogLine) { |
|
||||||
// Truncate long log lines.
|
|
||||||
// TODO(dsymonds): Check if this is still necessary.
|
|
||||||
const lim = 8 << 10 |
|
||||||
if len(*ll.Message) > lim { |
|
||||||
suffix := fmt.Sprintf("...(length %d)", len(*ll.Message)) |
|
||||||
ll.Message = proto.String((*ll.Message)[:lim-len(suffix)] + suffix) |
|
||||||
} |
|
||||||
|
|
||||||
c.pendingLogs.Lock() |
|
||||||
c.pendingLogs.lines = append(c.pendingLogs.lines, ll) |
|
||||||
c.pendingLogs.Unlock() |
|
||||||
} |
|
||||||
|
|
||||||
var logLevelName = map[int64]string{ |
|
||||||
0: "DEBUG", |
|
||||||
1: "INFO", |
|
||||||
2: "WARNING", |
|
||||||
3: "ERROR", |
|
||||||
4: "CRITICAL", |
|
||||||
} |
|
||||||
|
|
||||||
func logf(c *context, level int64, format string, args ...interface{}) { |
|
||||||
if c == nil { |
|
||||||
panic("not an App Engine context") |
|
||||||
} |
|
||||||
s := fmt.Sprintf(format, args...) |
|
||||||
s = strings.TrimRight(s, "\n") // Remove any trailing newline characters.
|
|
||||||
c.addLogLine(&logpb.UserAppLogLine{ |
|
||||||
TimestampUsec: proto.Int64(time.Now().UnixNano() / 1e3), |
|
||||||
Level: &level, |
|
||||||
Message: &s, |
|
||||||
}) |
|
||||||
log.Print(logLevelName[level] + ": " + s) |
|
||||||
} |
|
||||||
|
|
||||||
// flushLog attempts to flush any pending logs to the appserver.
|
|
||||||
// It should not be called concurrently.
|
|
||||||
func (c *context) flushLog(force bool) (flushed bool) { |
|
||||||
c.pendingLogs.Lock() |
|
||||||
// Grab up to 30 MB. We can get away with up to 32 MB, but let's be cautious.
|
|
||||||
n, rem := 0, 30<<20 |
|
||||||
for ; n < len(c.pendingLogs.lines); n++ { |
|
||||||
ll := c.pendingLogs.lines[n] |
|
||||||
// Each log line will require about 3 bytes of overhead.
|
|
||||||
nb := proto.Size(ll) + 3 |
|
||||||
if nb > rem { |
|
||||||
break |
|
||||||
} |
|
||||||
rem -= nb |
|
||||||
} |
|
||||||
lines := c.pendingLogs.lines[:n] |
|
||||||
c.pendingLogs.lines = c.pendingLogs.lines[n:] |
|
||||||
c.pendingLogs.Unlock() |
|
||||||
|
|
||||||
if len(lines) == 0 && !force { |
|
||||||
// Nothing to flush.
|
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
rescueLogs := false |
|
||||||
defer func() { |
|
||||||
if rescueLogs { |
|
||||||
c.pendingLogs.Lock() |
|
||||||
c.pendingLogs.lines = append(lines, c.pendingLogs.lines...) |
|
||||||
c.pendingLogs.Unlock() |
|
||||||
} |
|
||||||
}() |
|
||||||
|
|
||||||
buf, err := proto.Marshal(&logpb.UserAppLogGroup{ |
|
||||||
LogLine: lines, |
|
||||||
}) |
|
||||||
if err != nil { |
|
||||||
log.Printf("internal.flushLog: marshaling UserAppLogGroup: %v", err) |
|
||||||
rescueLogs = true |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
req := &logpb.FlushRequest{ |
|
||||||
Logs: buf, |
|
||||||
} |
|
||||||
res := &basepb.VoidProto{} |
|
||||||
c.pendingLogs.Lock() |
|
||||||
c.pendingLogs.flushes++ |
|
||||||
c.pendingLogs.Unlock() |
|
||||||
if err := Call(toContext(c), "logservice", "Flush", req, res); err != nil { |
|
||||||
log.Printf("internal.flushLog: Flush RPC: %v", err) |
|
||||||
rescueLogs = true |
|
||||||
return false |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
const ( |
|
||||||
// Log flushing parameters.
|
|
||||||
flushInterval = 1 * time.Second |
|
||||||
forceFlushInterval = 60 * time.Second |
|
||||||
) |
|
||||||
|
|
||||||
func (c *context) logFlusher(stop <-chan int) { |
|
||||||
lastFlush := time.Now() |
|
||||||
tick := time.NewTicker(flushInterval) |
|
||||||
for { |
|
||||||
select { |
|
||||||
case <-stop: |
|
||||||
// Request finished.
|
|
||||||
tick.Stop() |
|
||||||
return |
|
||||||
case <-tick.C: |
|
||||||
force := time.Now().Sub(lastFlush) > forceFlushInterval |
|
||||||
if c.flushLog(force) { |
|
||||||
lastFlush = time.Now() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func ContextForTesting(req *http.Request) netcontext.Context { |
|
||||||
return toContext(&context{req: req}) |
|
||||||
} |
|
@ -0,0 +1,11 @@ |
|||||||
|
// Copyright 2018 Google LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Apache 2.0
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build appenginevm
|
||||||
|
|
||||||
|
package internal |
||||||
|
|
||||||
|
func init() { |
||||||
|
appengineFlex = true |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
package internal |
||||||
|
|
||||||
|
// MainPath stores the file path of the main package. On App Engine Standard
|
||||||
|
// using Go version 1.9 and below, this will be unset. On App Engine Flex and
|
||||||
|
// App Engine Standard second-gen (Go 1.11 and above), this will be the
|
||||||
|
// filepath to package main.
|
||||||
|
var MainPath string |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue