anggabayu
6 years ago
commit
1aa1cc1360
35 changed files with 20841 additions and 0 deletions
Binary file not shown.
@ -0,0 +1,21 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2014 Nate Finch |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
of this software and associated documentation files (the "Software"), to deal |
||||||
|
in the Software without restriction, including without limitation the rights |
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
copies of the Software, and to permit persons to whom the Software is |
||||||
|
furnished to do so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,11 @@ |
|||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package lumberjack |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
func chown(_ string, _ os.FileInfo) error { |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
package lumberjack |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"syscall" |
||||||
|
) |
||||||
|
|
||||||
|
// os_Chown is a var so we can mock it out during tests.
|
||||||
|
var os_Chown = os.Chown |
||||||
|
|
||||||
|
func chown(name string, info os.FileInfo) error { |
||||||
|
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
f.Close() |
||||||
|
stat := info.Sys().(*syscall.Stat_t) |
||||||
|
return os_Chown(name, int(stat.Uid), int(stat.Gid)) |
||||||
|
} |
@ -0,0 +1,543 @@ |
|||||||
|
// Package lumberjack provides a rolling logger.
|
||||||
|
//
|
||||||
|
// Note that this is v2.0 of lumberjack, and should be imported using gopkg.in
|
||||||
|
// thusly:
|
||||||
|
//
|
||||||
|
// import "gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
//
|
||||||
|
// The package name remains simply lumberjack, and the code resides at
|
||||||
|
// https://github.com/natefinch/lumberjack under the v2.0 branch.
|
||||||
|
//
|
||||||
|
// Lumberjack is intended to be one part of a logging infrastructure.
|
||||||
|
// It is not an all-in-one solution, but instead is a pluggable
|
||||||
|
// component at the bottom of the logging stack that simply controls the files
|
||||||
|
// to which logs are written.
|
||||||
|
//
|
||||||
|
// Lumberjack plays well with any logging package that can write to an
|
||||||
|
// io.Writer, including the standard library's log package.
|
||||||
|
//
|
||||||
|
// Lumberjack assumes that only one process is writing to the output files.
|
||||||
|
// Using the same lumberjack configuration from multiple processes on the same
|
||||||
|
// machine will result in improper behavior.
|
||||||
|
package lumberjack |
||||||
|
|
||||||
|
import ( |
||||||
|
"compress/gzip" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"sort" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
backupTimeFormat = "2006-01-02T15-04-05.000" |
||||||
|
compressSuffix = ".gz" |
||||||
|
defaultMaxSize = 100 |
||||||
|
) |
||||||
|
|
||||||
|
// ensure we always implement io.WriteCloser
|
||||||
|
var _ io.WriteCloser = (*Logger)(nil) |
||||||
|
|
||||||
|
// Logger is an io.WriteCloser that writes to the specified filename.
|
||||||
|
//
|
||||||
|
// Logger opens or creates the logfile on first Write. If the file exists and
|
||||||
|
// is less than MaxSize megabytes, lumberjack will open and append to that file.
|
||||||
|
// If the file exists and its size is >= MaxSize megabytes, the file is renamed
|
||||||
|
// by putting the current time in a timestamp in the name immediately before the
|
||||||
|
// file's extension (or the end of the filename if there's no extension). A new
|
||||||
|
// log file is then created using original filename.
|
||||||
|
//
|
||||||
|
// Whenever a write would cause the current log file exceed MaxSize megabytes,
|
||||||
|
// the current file is closed, renamed, and a new log file created with the
|
||||||
|
// original name. Thus, the filename you give Logger is always the "current" log
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// Backups use the log file name given to Logger, in the form
|
||||||
|
// `name-timestamp.ext` where name is the filename without the extension,
|
||||||
|
// timestamp is the time at which the log was rotated formatted with the
|
||||||
|
// time.Time format of `2006-01-02T15-04-05.000` and the extension is the
|
||||||
|
// original extension. For example, if your Logger.Filename is
|
||||||
|
// `/var/log/foo/server.log`, a backup created at 6:30pm on Nov 11 2016 would
|
||||||
|
// use the filename `/var/log/foo/server-2016-11-04T18-30-00.000.log`
|
||||||
|
//
|
||||||
|
// Cleaning Up Old Log Files
|
||||||
|
//
|
||||||
|
// Whenever a new logfile gets created, old log files may be deleted. The most
|
||||||
|
// recent files according to the encoded timestamp will be retained, up to a
|
||||||
|
// number equal to MaxBackups (or all of them if MaxBackups is 0). Any files
|
||||||
|
// with an encoded timestamp older than MaxAge days are deleted, regardless of
|
||||||
|
// MaxBackups. Note that the time encoded in the timestamp is the rotation
|
||||||
|
// time, which may differ from the last time that file was written to.
|
||||||
|
//
|
||||||
|
// If MaxBackups and MaxAge are both 0, no old log files will be deleted.
|
||||||
|
type Logger struct { |
||||||
|
// Filename is the file to write logs to. Backup log files will be retained
|
||||||
|
// in the same directory. It uses <processname>-lumberjack.log in
|
||||||
|
// os.TempDir() if empty.
|
||||||
|
Filename string `json:"filename" yaml:"filename"` |
||||||
|
|
||||||
|
// MaxSize is the maximum size in megabytes of the log file before it gets
|
||||||
|
// rotated. It defaults to 100 megabytes.
|
||||||
|
MaxSize int `json:"maxsize" yaml:"maxsize"` |
||||||
|
|
||||||
|
// MaxAge is the maximum number of days to retain old log files based on the
|
||||||
|
// timestamp encoded in their filename. Note that a day is defined as 24
|
||||||
|
// hours and may not exactly correspond to calendar days due to daylight
|
||||||
|
// savings, leap seconds, etc. The default is not to remove old log files
|
||||||
|
// based on age.
|
||||||
|
MaxAge int `json:"maxage" yaml:"maxage"` |
||||||
|
|
||||||
|
// MaxBackups is the maximum number of old log files to retain. The default
|
||||||
|
// is to retain all old log files (though MaxAge may still cause them to get
|
||||||
|
// deleted.)
|
||||||
|
MaxBackups int `json:"maxbackups" yaml:"maxbackups"` |
||||||
|
|
||||||
|
// LocalTime determines if the time used for formatting the timestamps in
|
||||||
|
// backup files is the computer's local time. The default is to use UTC
|
||||||
|
// time.
|
||||||
|
LocalTime bool `json:"localtime" yaml:"localtime"` |
||||||
|
|
||||||
|
// Compress determines if the rotated log files should be compressed
|
||||||
|
// using gzip. The default is not to perform compression.
|
||||||
|
Compress bool `json:"compress" yaml:"compress"` |
||||||
|
|
||||||
|
size int64 |
||||||
|
file *os.File |
||||||
|
mu sync.Mutex |
||||||
|
|
||||||
|
millCh chan bool |
||||||
|
startMill sync.Once |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
// currentTime exists so it can be mocked out by tests.
|
||||||
|
currentTime = time.Now |
||||||
|
|
||||||
|
// os_Stat exists so it can be mocked out by tests.
|
||||||
|
os_Stat = os.Stat |
||||||
|
|
||||||
|
// megabyte is the conversion factor between MaxSize and bytes. It is a
|
||||||
|
// variable so tests can mock it out and not need to write megabytes of data
|
||||||
|
// to disk.
|
||||||
|
megabyte = 1024 * 1024 |
||||||
|
) |
||||||
|
|
||||||
|
// Write implements io.Writer. If a write would cause the log file to be larger
|
||||||
|
// than MaxSize, the file is closed, renamed to include a timestamp of the
|
||||||
|
// current time, and a new log file is created using the original log file name.
|
||||||
|
// If the length of the write is greater than MaxSize, an error is returned.
|
||||||
|
func (l *Logger) Write(p []byte) (n int, err error) { |
||||||
|
l.mu.Lock() |
||||||
|
defer l.mu.Unlock() |
||||||
|
|
||||||
|
writeLen := int64(len(p)) |
||||||
|
if writeLen > l.max() { |
||||||
|
return 0, fmt.Errorf( |
||||||
|
"write length %d exceeds maximum file size %d", writeLen, l.max(), |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
if l.file == nil { |
||||||
|
if err = l.openExistingOrNew(len(p)); err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if l.size+writeLen > l.max() { |
||||||
|
if err := l.rotate(); err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
n, err = os.Stderr.Write(p) |
||||||
|
|
||||||
|
n, err = l.file.Write(p) |
||||||
|
l.size += int64(n) |
||||||
|
|
||||||
|
return n, err |
||||||
|
} |
||||||
|
|
||||||
|
// Close implements io.Closer, and closes the current logfile.
|
||||||
|
func (l *Logger) Close() error { |
||||||
|
l.mu.Lock() |
||||||
|
defer l.mu.Unlock() |
||||||
|
return l.close() |
||||||
|
} |
||||||
|
|
||||||
|
// close closes the file if it is open.
|
||||||
|
func (l *Logger) close() error { |
||||||
|
if l.file == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
err := l.file.Close() |
||||||
|
l.file = nil |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Rotate causes Logger to close the existing log file and immediately create a
|
||||||
|
// new one. This is a helper function for applications that want to initiate
|
||||||
|
// rotations outside of the normal rotation rules, such as in response to
|
||||||
|
// SIGHUP. After rotating, this initiates compression and removal of old log
|
||||||
|
// files according to the configuration.
|
||||||
|
func (l *Logger) Rotate() error { |
||||||
|
l.mu.Lock() |
||||||
|
defer l.mu.Unlock() |
||||||
|
return l.rotate() |
||||||
|
} |
||||||
|
|
||||||
|
// rotate closes the current file, moves it aside with a timestamp in the name,
|
||||||
|
// (if it exists), opens a new file with the original filename, and then runs
|
||||||
|
// post-rotation processing and removal.
|
||||||
|
func (l *Logger) rotate() error { |
||||||
|
if err := l.close(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := l.openNew(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
l.mill() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// openNew opens a new log file for writing, moving any old log file out of the
|
||||||
|
// way. This methods assumes the file has already been closed.
|
||||||
|
func (l *Logger) openNew() error { |
||||||
|
err := os.MkdirAll(l.dir(), 0744) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("can't make directories for new logfile: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
name := l.filename() |
||||||
|
mode := os.FileMode(0644) |
||||||
|
info, err := os_Stat(name) |
||||||
|
if err == nil { |
||||||
|
// Copy the mode off the old logfile.
|
||||||
|
mode = info.Mode() |
||||||
|
// move the existing file
|
||||||
|
newname := backupName(name, l.LocalTime) |
||||||
|
if err := os.Rename(name, newname); err != nil { |
||||||
|
return fmt.Errorf("can't rename log file: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
// this is a no-op anywhere but linux
|
||||||
|
if err := chown(name, info); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// we use truncate here because this should only get called when we've moved
|
||||||
|
// the file ourselves. if someone else creates the file in the meantime,
|
||||||
|
// just wipe out the contents.
|
||||||
|
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("can't open new logfile: %s", err) |
||||||
|
} |
||||||
|
l.file = f |
||||||
|
l.size = 0 |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// backupName creates a new filename from the given name, inserting a timestamp
|
||||||
|
// between the filename and the extension, using the local time if requested
|
||||||
|
// (otherwise UTC).
|
||||||
|
func backupName(name string, local bool) string { |
||||||
|
dir := filepath.Dir(name) |
||||||
|
filename := filepath.Base(name) |
||||||
|
ext := filepath.Ext(filename) |
||||||
|
prefix := filename[:len(filename)-len(ext)] |
||||||
|
t := currentTime() |
||||||
|
if !local { |
||||||
|
t = t.UTC() |
||||||
|
} |
||||||
|
|
||||||
|
timestamp := t.Format(backupTimeFormat) |
||||||
|
return filepath.Join(dir, fmt.Sprintf("%s-%s%s", prefix, timestamp, ext)) |
||||||
|
} |
||||||
|
|
||||||
|
// openExistingOrNew opens the logfile if it exists and if the current write
|
||||||
|
// would not put it over MaxSize. If there is no such file or the write would
|
||||||
|
// put it over the MaxSize, a new file is created.
|
||||||
|
func (l *Logger) openExistingOrNew(writeLen int) error { |
||||||
|
l.mill() |
||||||
|
|
||||||
|
filename := l.filename() |
||||||
|
info, err := os_Stat(filename) |
||||||
|
if os.IsNotExist(err) { |
||||||
|
return l.openNew() |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("error getting log file info: %s", err) |
||||||
|
} |
||||||
|
|
||||||
|
if info.Size()+int64(writeLen) >= l.max() { |
||||||
|
return l.rotate() |
||||||
|
} |
||||||
|
|
||||||
|
file, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644) |
||||||
|
if err != nil { |
||||||
|
// if we fail to open the old log file for some reason, just ignore
|
||||||
|
// it and open a new log file.
|
||||||
|
return l.openNew() |
||||||
|
} |
||||||
|
l.file = file |
||||||
|
l.size = info.Size() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// genFilename generates the name of the logfile from the current time.
|
||||||
|
func (l *Logger) filename() string { |
||||||
|
if l.Filename != "" { |
||||||
|
return l.Filename |
||||||
|
} |
||||||
|
name := filepath.Base(os.Args[0]) + "-lumberjack.log" |
||||||
|
return filepath.Join(os.TempDir(), name) |
||||||
|
} |
||||||
|
|
||||||
|
// millRunOnce performs compression and removal of stale log files.
|
||||||
|
// Log files are compressed if enabled via configuration and old log
|
||||||
|
// files are removed, keeping at most l.MaxBackups files, as long as
|
||||||
|
// none of them are older than MaxAge.
|
||||||
|
func (l *Logger) millRunOnce() error { |
||||||
|
if l.MaxBackups == 0 && l.MaxAge == 0 && !l.Compress { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
files, err := l.oldLogFiles() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
var compress, remove []logInfo |
||||||
|
|
||||||
|
if l.MaxBackups > 0 && l.MaxBackups < len(files) { |
||||||
|
preserved := make(map[string]bool) |
||||||
|
var remaining []logInfo |
||||||
|
for _, f := range files { |
||||||
|
// Only count the uncompressed log file or the
|
||||||
|
// compressed log file, not both.
|
||||||
|
fn := f.Name() |
||||||
|
if strings.HasSuffix(fn, compressSuffix) { |
||||||
|
fn = fn[:len(fn)-len(compressSuffix)] |
||||||
|
} |
||||||
|
preserved[fn] = true |
||||||
|
|
||||||
|
if len(preserved) > l.MaxBackups { |
||||||
|
remove = append(remove, f) |
||||||
|
} else { |
||||||
|
remaining = append(remaining, f) |
||||||
|
} |
||||||
|
} |
||||||
|
files = remaining |
||||||
|
} |
||||||
|
if l.MaxAge > 0 { |
||||||
|
diff := time.Duration(int64(24*time.Hour) * int64(l.MaxAge)) |
||||||
|
cutoff := currentTime().Add(-1 * diff) |
||||||
|
|
||||||
|
var remaining []logInfo |
||||||
|
for _, f := range files { |
||||||
|
if f.timestamp.Before(cutoff) { |
||||||
|
remove = append(remove, f) |
||||||
|
} else { |
||||||
|
remaining = append(remaining, f) |
||||||
|
} |
||||||
|
} |
||||||
|
files = remaining |
||||||
|
} |
||||||
|
|
||||||
|
if l.Compress { |
||||||
|
for _, f := range files { |
||||||
|
if !strings.HasSuffix(f.Name(), compressSuffix) { |
||||||
|
compress = append(compress, f) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for _, f := range remove { |
||||||
|
errRemove := os.Remove(filepath.Join(l.dir(), f.Name())) |
||||||
|
if err == nil && errRemove != nil { |
||||||
|
err = errRemove |
||||||
|
} |
||||||
|
} |
||||||
|
for _, f := range compress { |
||||||
|
fn := filepath.Join(l.dir(), f.Name()) |
||||||
|
errCompress := compressLogFile(fn, fn+compressSuffix) |
||||||
|
if err == nil && errCompress != nil { |
||||||
|
err = errCompress |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// millRun runs in a goroutine to manage post-rotation compression and removal
|
||||||
|
// of old log files.
|
||||||
|
func (l *Logger) millRun() { |
||||||
|
for _ = range l.millCh { |
||||||
|
// what am I going to do, log this?
|
||||||
|
_ = l.millRunOnce() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// mill performs post-rotation compression and removal of stale log files,
|
||||||
|
// starting the mill goroutine if necessary.
|
||||||
|
func (l *Logger) mill() { |
||||||
|
l.startMill.Do(func() { |
||||||
|
l.millCh = make(chan bool, 1) |
||||||
|
go l.millRun() |
||||||
|
}) |
||||||
|
select { |
||||||
|
case l.millCh <- true: |
||||||
|
default: |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// oldLogFiles returns the list of backup log files stored in the same
|
||||||
|
// directory as the current log file, sorted by ModTime
|
||||||
|
func (l *Logger) oldLogFiles() ([]logInfo, error) { |
||||||
|
files, err := ioutil.ReadDir(l.dir()) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("can't read log file directory: %s", err) |
||||||
|
} |
||||||
|
logFiles := []logInfo{} |
||||||
|
|
||||||
|
prefix, ext := l.prefixAndExt() |
||||||
|
|
||||||
|
for _, f := range files { |
||||||
|
if f.IsDir() { |
||||||
|
continue |
||||||
|
} |
||||||
|
if t, err := l.timeFromName(f.Name(), prefix, ext); err == nil { |
||||||
|
logFiles = append(logFiles, logInfo{t, f}) |
||||||
|
continue |
||||||
|
} |
||||||
|
if t, err := l.timeFromName(f.Name(), prefix, ext+compressSuffix); err == nil { |
||||||
|
logFiles = append(logFiles, logInfo{t, f}) |
||||||
|
continue |
||||||
|
} |
||||||
|
// error parsing means that the suffix at the end was not generated
|
||||||
|
// by lumberjack, and therefore it's not a backup file.
|
||||||
|
} |
||||||
|
|
||||||
|
sort.Sort(byFormatTime(logFiles)) |
||||||
|
|
||||||
|
return logFiles, nil |
||||||
|
} |
||||||
|
|
||||||
|
// timeFromName extracts the formatted time from the filename by stripping off
|
||||||
|
// the filename's prefix and extension. This prevents someone's filename from
|
||||||
|
// confusing time.parse.
|
||||||
|
func (l *Logger) timeFromName(filename, prefix, ext string) (time.Time, error) { |
||||||
|
if !strings.HasPrefix(filename, prefix) { |
||||||
|
return time.Time{}, errors.New("mismatched prefix") |
||||||
|
} |
||||||
|
if !strings.HasSuffix(filename, ext) { |
||||||
|
return time.Time{}, errors.New("mismatched extension") |
||||||
|
} |
||||||
|
ts := filename[len(prefix) : len(filename)-len(ext)] |
||||||
|
return time.Parse(backupTimeFormat, ts) |
||||||
|
} |
||||||
|
|
||||||
|
// max returns the maximum size in bytes of log files before rolling.
|
||||||
|
func (l *Logger) max() int64 { |
||||||
|
if l.MaxSize == 0 { |
||||||
|
return int64(defaultMaxSize * megabyte) |
||||||
|
} |
||||||
|
return int64(l.MaxSize) * int64(megabyte) |
||||||
|
} |
||||||
|
|
||||||
|
// dir returns the directory for the current filename.
|
||||||
|
func (l *Logger) dir() string { |
||||||
|
return filepath.Dir(l.filename()) |
||||||
|
} |
||||||
|
|
||||||
|
// prefixAndExt returns the filename part and extension part from the Logger's
|
||||||
|
// filename.
|
||||||
|
func (l *Logger) prefixAndExt() (prefix, ext string) { |
||||||
|
filename := filepath.Base(l.filename()) |
||||||
|
ext = filepath.Ext(filename) |
||||||
|
prefix = filename[:len(filename)-len(ext)] + "-" |
||||||
|
return prefix, ext |
||||||
|
} |
||||||
|
|
||||||
|
// compressLogFile compresses the given log file, removing the
|
||||||
|
// uncompressed log file if successful.
|
||||||
|
func compressLogFile(src, dst string) (err error) { |
||||||
|
f, err := os.Open(src) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to open log file: %v", err) |
||||||
|
} |
||||||
|
defer f.Close() |
||||||
|
|
||||||
|
fi, err := os_Stat(src) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to stat log file: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
if err := chown(dst, fi); err != nil { |
||||||
|
return fmt.Errorf("failed to chown compressed log file: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// If this file already exists, we presume it was created by
|
||||||
|
// a previous attempt to compress the log file.
|
||||||
|
gzf, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode()) |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("failed to open compressed log file: %v", err) |
||||||
|
} |
||||||
|
defer gzf.Close() |
||||||
|
|
||||||
|
gz := gzip.NewWriter(gzf) |
||||||
|
|
||||||
|
defer func() { |
||||||
|
if err != nil { |
||||||
|
os.Remove(dst) |
||||||
|
err = fmt.Errorf("failed to compress log file: %v", err) |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
if _, err := io.Copy(gz, f); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := gz.Close(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := gzf.Close(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if err := f.Close(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := os.Remove(src); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// logInfo is a convenience struct to return the filename and its embedded
|
||||||
|
// timestamp.
|
||||||
|
type logInfo struct { |
||||||
|
timestamp time.Time |
||||||
|
os.FileInfo |
||||||
|
} |
||||||
|
|
||||||
|
// byFormatTime sorts by newest time formatted in the name.
|
||||||
|
type byFormatTime []logInfo |
||||||
|
|
||||||
|
func (b byFormatTime) Less(i, j int) bool { |
||||||
|
return b[i].timestamp.After(b[j].timestamp) |
||||||
|
} |
||||||
|
|
||||||
|
func (b byFormatTime) Swap(i, j int) { |
||||||
|
b[i], b[j] = b[j], b[i] |
||||||
|
} |
||||||
|
|
||||||
|
func (b byFormatTime) Len() int { |
||||||
|
return len(b) |
||||||
|
} |
@ -0,0 +1,247 @@ |
|||||||
|
/* |
||||||
|
Model for standard/IU records |
||||||
|
*/ |
||||||
|
|
||||||
|
package model |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/gob" |
||||||
|
"encoding/json" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
/* |
||||||
|
Standard tags for several properties |
||||||
|
*/ |
||||||
|
const ( |
||||||
|
TAG_TIMESTAMP = 1 |
||||||
|
TAG_PRIORITY = 2 |
||||||
|
TAG_LONGITUDE = 3 |
||||||
|
TAG_LATITUDE = 4 |
||||||
|
TAG_ALTITUDE = 5 |
||||||
|
TAG_ANGLE = 6 |
||||||
|
TAG_SATELITES = 7 |
||||||
|
TAG_SPEED = 8 |
||||||
|
TAG_HDOP = 9 |
||||||
|
TAG_EVENT_SRC = 10 |
||||||
|
|
||||||
|
DEV_ONLINE = 1 |
||||||
|
DEV_OFFLINE = 0 |
||||||
|
|
||||||
|
CMD_RUP_MODULE = 0 |
||||||
|
CMD_RUP_RECORD = 1 |
||||||
|
CMD_RUP_RECORD_RESP = 100 |
||||||
|
CMD_RUP_CONFIG = 2 |
||||||
|
CMD_RUP_CONFIG_RESP = 102 |
||||||
|
CMD_RUP_VER = 3 |
||||||
|
CMD_RUP_VER_RESP = 103 |
||||||
|
CMD_RUP_FIRMW = 4 |
||||||
|
CMD_RUP_FIRMW_RESP = 104 |
||||||
|
CMD_RUP_SCARD = 5 |
||||||
|
CMD_RUP_SCARD_RESP = 107 |
||||||
|
CMD_RUP_SCARDZ = 6 |
||||||
|
CMD_RUP_SCARDZ_RESP = 107 |
||||||
|
CMD_RUP_SMGPRS = 7 |
||||||
|
CMD_RUP_SMGPRS_RESP = 108 |
||||||
|
CMD_RUP_DIAG = 9 |
||||||
|
CMD_RUP_DIAG_RESP = 109 |
||||||
|
CMD_RUP_TACHOC = 10 |
||||||
|
CMD_RUP_TACHOC_RESP = 110 |
||||||
|
CMD_RUP_TACHOD = 11 |
||||||
|
CMD_RUP_TACHOD_RESP = 111 |
||||||
|
CMD_RUP_TACHOI = 12 |
||||||
|
CMD_RUP_TACHOI_RESP = 111 |
||||||
|
CMD_RUP_TRCH = 14 |
||||||
|
CMD_RUP_TRCH_RESP = 114 |
||||||
|
CMD_RUP_GARMR = 30 |
||||||
|
CMD_RUP_GARMR_RESP = 130 |
||||||
|
CMD_RUP_GARMD = 31 |
||||||
|
CMD_RUP_GARMD_RESP = 131 |
||||||
|
CMD_RUP_CONN_PARM = 105 |
||||||
|
CMD_RUP_ODO = 106 |
||||||
|
) |
||||||
|
|
||||||
|
// Device and standard Tag type
|
||||||
|
type DevTag uint16 |
||||||
|
type StdTag uint16 |
||||||
|
type IOMap map[DevTag]StdTag |
||||||
|
|
||||||
|
type DeviceData struct { |
||||||
|
Command byte |
||||||
|
Data []byte |
||||||
|
} |
||||||
|
|
||||||
|
// Data structure for Device I/O record (8-bit)
|
||||||
|
type DeviceIo8 struct { |
||||||
|
Tag StdTag |
||||||
|
Value int8 |
||||||
|
} |
||||||
|
|
||||||
|
// Data structure for Device I/O record (16-bit)
|
||||||
|
type DeviceIo16 struct { |
||||||
|
Tag StdTag |
||||||
|
Value int16 |
||||||
|
} |
||||||
|
|
||||||
|
// Data structure for Device I/O record (32-bit)
|
||||||
|
type DeviceIo32 struct { |
||||||
|
Tag StdTag |
||||||
|
Value int32 |
||||||
|
} |
||||||
|
|
||||||
|
// Data structure for Device I/O record (64-bit)
|
||||||
|
type DeviceIo64 struct { |
||||||
|
Tag StdTag |
||||||
|
Value int64 |
||||||
|
} |
||||||
|
|
||||||
|
// Data structure for real/double I/O
|
||||||
|
type DeviceIoReal struct { |
||||||
|
Tag StdTag |
||||||
|
Value float64 |
||||||
|
} |
||||||
|
|
||||||
|
// Data structure for string I/O
|
||||||
|
type DeviceIoString struct { |
||||||
|
Tag StdTag |
||||||
|
Value string |
||||||
|
} |
||||||
|
|
||||||
|
// IO group
|
||||||
|
type DeviceIo struct { |
||||||
|
V8 []DeviceIo8 |
||||||
|
V16 []DeviceIo16 |
||||||
|
V32 []DeviceIo32 |
||||||
|
V64 []DeviceIo64 |
||||||
|
VReal []DeviceIoReal |
||||||
|
VStr []DeviceIoString |
||||||
|
} |
||||||
|
|
||||||
|
// Data structure for standard IU records
|
||||||
|
type DeviceRecord struct { |
||||||
|
Timestamp time.Time |
||||||
|
Priority byte |
||||||
|
Longitude float64 |
||||||
|
Latitude float64 |
||||||
|
Altitude float32 |
||||||
|
Angle float32 |
||||||
|
Satelites byte |
||||||
|
Speed float32 |
||||||
|
IO DeviceIo |
||||||
|
} |
||||||
|
|
||||||
|
// Basic device information
|
||||||
|
type DeviceDescriptor struct { |
||||||
|
IMEI uint64 |
||||||
|
QoS byte |
||||||
|
Id uint64 |
||||||
|
Model string |
||||||
|
} |
||||||
|
|
||||||
|
// Collection of data readed from device
|
||||||
|
type DeviceRecords struct { |
||||||
|
TimeReceived time.Time |
||||||
|
Descriptor DeviceDescriptor |
||||||
|
Records []DeviceRecord |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
type DeviceIoMapping struct { |
||||||
|
Descriptor DeviceDescriptor |
||||||
|
IOMapping map[DevTag]StdTag |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
type DisplayRequest struct { |
||||||
|
Timestamp time.Time |
||||||
|
IMEI uint64 |
||||||
|
PortID byte |
||||||
|
Data []byte |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
|
//Parser interface that must be implemented by device
|
||||||
|
type Parser interface { |
||||||
|
GetMaxPacketSize() int |
||||||
|
GetMinPacketSize() int |
||||||
|
GetBufferSize() int |
||||||
|
GetStream() []byte |
||||||
|
Payload() []byte |
||||||
|
GetError() error |
||||||
|
GetCommand() byte |
||||||
|
// GetRecords() *DeviceRecords
|
||||||
|
GetIMEI() uint64 |
||||||
|
GetClientResponse() []byte |
||||||
|
ExecuteAsync() |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
|
//Convert record to/from stream
|
||||||
|
type RecordsConverter struct { |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert records to JSON |
||||||
|
*/ |
||||||
|
func (self RecordsConverter) ToJson(recs *DeviceRecords) (jstr string, err error) { |
||||||
|
bin, err := json.Marshal(recs) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
|
||||||
|
jstr = string(bin) |
||||||
|
return jstr, nil |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert json to records |
||||||
|
*/ |
||||||
|
func (self RecordsConverter) FromJson(jstr string) (recs *DeviceRecords, err error) { |
||||||
|
var devRec DeviceRecords |
||||||
|
err = json.Unmarshal([]byte(jstr), &devRec) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
recs = &devRec |
||||||
|
return recs, nil |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert device records to bytes |
||||||
|
*/ |
||||||
|
func (self RecordsConverter) ToStream(recs *DeviceRecords) (stream []byte, err error) { |
||||||
|
var buf bytes.Buffer // Stand-in for byte stream buffer
|
||||||
|
enc := gob.NewEncoder(&buf) // Will write stream.
|
||||||
|
|
||||||
|
// Encode (send) some values.
|
||||||
|
err = enc.Encode(recs) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
//Gob bytes
|
||||||
|
stream = buf.Bytes() |
||||||
|
return stream, nil |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert records from stream |
||||||
|
*/ |
||||||
|
func (self RecordsConverter) FromStream(stream []byte) (recs *DeviceRecords, err error) { |
||||||
|
buf := bytes.NewBuffer(stream) |
||||||
|
dec := gob.NewDecoder(buf) // Will read from buffer.
|
||||||
|
|
||||||
|
// Decode (receive) and print the values.
|
||||||
|
var dpkt DeviceRecords |
||||||
|
err = dec.Decode(&dpkt) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
recs = &dpkt |
||||||
|
|
||||||
|
return recs, nil |
||||||
|
} |
@ -0,0 +1,468 @@ |
|||||||
|
//Message model
|
||||||
|
/* |
||||||
|
Table "public.bcs_list" |
||||||
|
Column | Type | Modifiers |
||||||
|
----------------+-----------------------------+------------------------------------------------------- |
||||||
|
id | bigint | not null default nextval('bcs_list_id_seq'::regclass) |
||||||
|
bcs_message_id | bigint | |
||||||
|
imei | character varying(25) | |
||||||
|
sent_ts | timestamp without time zone | |
||||||
|
delivered_ts | timestamp without time zone | |
||||||
|
Indexes: |
||||||
|
"bcs_list_pkey" PRIMARY KEY, btree (id) |
||||||
|
Foreign-key constraints: |
||||||
|
"bcs_list_bcs_message_id_fkey" FOREIGN KEY (bcs_message_id) REFERENCES bcs_message(id) |
||||||
|
|
||||||
|
Table "public.bcs_message" |
||||||
|
Column | Type | Modifiers |
||||||
|
------------------+-----------------------------+---------------------------------------------------------- |
||||||
|
id | bigint | not null default nextval('bcs_message_id_seq'::regclass) |
||||||
|
bcs_message_time | timestamp without time zone | |
||||||
|
duration | integer | |
||||||
|
mbuzzer | integer | |
||||||
|
message | text | |
||||||
|
expired | timestamp without time zone | |
||||||
|
company_id | bigint | |
||||||
|
Indexes: |
||||||
|
"bcs_message_pkey" PRIMARY KEY, btree (id) |
||||||
|
Foreign-key constraints: |
||||||
|
"bcs_message_company_id_fkey" FOREIGN KEY (company_id) REFERENCES m_company(id) |
||||||
|
Referenced by: |
||||||
|
TABLE "bcs_list" CONSTRAINT "bcs_list_bcs_message_id_fkey" FOREIGN KEY (bcs_message_id) REFERENCES bcs_message(id) |
||||||
|
*/ |
||||||
|
|
||||||
|
package model |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"log" |
||||||
|
"net/http" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/ipsusila/opt" |
||||||
|
) |
||||||
|
|
||||||
|
//Message status report
|
||||||
|
const ( |
||||||
|
MessageError = -1 |
||||||
|
MessageSent = 1 |
||||||
|
MessageDelivered = 2 |
||||||
|
) |
||||||
|
|
||||||
|
//ISOTime encapsulate timestamp for JSON parsing
|
||||||
|
type ISOTime struct { |
||||||
|
time.Time |
||||||
|
} |
||||||
|
|
||||||
|
//UserMessage retrieved from GUI
|
||||||
|
type UserMessage struct { |
||||||
|
Vehicles []string `json:"vehicles"` |
||||||
|
Duration int `json:"duration"` |
||||||
|
NBuzzer int `json:"nbuzzer"` |
||||||
|
Message string `json:"message"` |
||||||
|
Expired ISOTime `json:"expired"` |
||||||
|
} |
||||||
|
|
||||||
|
//BroadcastMessage holds message information need to be broadcast
|
||||||
|
type BroadcastMessage struct { |
||||||
|
Timestamp ISOTime `json:"ts"` |
||||||
|
Duration int `json:"duration"` |
||||||
|
NBuzzer int `json:"nbuzzer"` |
||||||
|
Message string `json:"message"` |
||||||
|
Expired ISOTime `json:"expired"` |
||||||
|
} |
||||||
|
|
||||||
|
//MessageStatus for specific IMEI
|
||||||
|
type MessageStatus struct { |
||||||
|
IMEI string `json:"imei"` |
||||||
|
Status string `json:"status"` |
||||||
|
Timestamp string `json:"timestamp"` |
||||||
|
StatusCode int `json:"-"` |
||||||
|
} |
||||||
|
|
||||||
|
//MessageProvider send/receive message
|
||||||
|
type MessageProvider interface { |
||||||
|
Get(imei uint64) (*BroadcastMessage, error) |
||||||
|
Put(message *UserMessage) error |
||||||
|
Update(status *MessageStatus) error |
||||||
|
} |
||||||
|
|
||||||
|
type messageSet struct { |
||||||
|
sync.RWMutex |
||||||
|
messages []*BroadcastMessage |
||||||
|
readPos int |
||||||
|
writePos int |
||||||
|
capacity int |
||||||
|
numItems int |
||||||
|
} |
||||||
|
|
||||||
|
//DbMessageProvider provides message with DB storage
|
||||||
|
type DbMessageProvider struct { |
||||||
|
sync.RWMutex |
||||||
|
queueMsg map[uint64]*messageSet |
||||||
|
sentMsg map[uint64]*messageSet |
||||||
|
capacity int |
||||||
|
saveToDb bool |
||||||
|
loadFromDb bool |
||||||
|
messageAPI string |
||||||
|
reportAPI string |
||||||
|
serviceTimeout time.Duration |
||||||
|
appendIMEI bool |
||||||
|
serviceClient *http.Client |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
//UnmarshalJSON convert string to timestamp
|
||||||
|
//Convet from Y-m-d H:M:S
|
||||||
|
func (tm *ISOTime) UnmarshalJSON(b []byte) (err error) { |
||||||
|
//TODO time other than local
|
||||||
|
s := strings.Trim(string(b), "\"") |
||||||
|
tm.Time, err = time.ParseInLocation("2006-01-02 15:04:05", s, time.Local) |
||||||
|
if err != nil { |
||||||
|
tm.Time = time.Time{} |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------------------------------------
|
||||||
|
func newMessageSet(capacity int) *messageSet { |
||||||
|
sets := &messageSet{ |
||||||
|
capacity: capacity, |
||||||
|
numItems: 0, |
||||||
|
readPos: 0, |
||||||
|
writePos: 0, |
||||||
|
messages: make([]*BroadcastMessage, capacity), |
||||||
|
} |
||||||
|
|
||||||
|
return sets |
||||||
|
} |
||||||
|
|
||||||
|
func (q *messageSet) doPut(msg *BroadcastMessage) bool { |
||||||
|
//check count (full or not?)
|
||||||
|
q.Lock() |
||||||
|
defer q.Unlock() |
||||||
|
|
||||||
|
if q.numItems == q.capacity { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
msg.Timestamp.Time = time.Now() |
||||||
|
pos := q.writePos % q.capacity |
||||||
|
q.messages[pos] = msg |
||||||
|
q.writePos = pos + 1 |
||||||
|
q.numItems++ |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (q *messageSet) put(msg *BroadcastMessage) bool { |
||||||
|
//check message pointer
|
||||||
|
if msg == nil { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return q.doPut(msg) |
||||||
|
} |
||||||
|
func (q *messageSet) putUnique(msg *BroadcastMessage) bool { |
||||||
|
//check message pointer
|
||||||
|
if msg == nil || q.contains(msg) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return q.doPut(msg) |
||||||
|
} |
||||||
|
|
||||||
|
func (q *messageSet) take() *BroadcastMessage { |
||||||
|
q.Lock() |
||||||
|
defer q.Unlock() |
||||||
|
|
||||||
|
//check count (empty or not?)
|
||||||
|
if q.numItems == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
pos := q.readPos % q.capacity |
||||||
|
msg := q.messages[pos] |
||||||
|
q.readPos = pos + 1 |
||||||
|
q.numItems-- |
||||||
|
|
||||||
|
return msg |
||||||
|
} |
||||||
|
|
||||||
|
func (q *messageSet) get() *BroadcastMessage { |
||||||
|
q.Lock() |
||||||
|
defer q.Unlock() |
||||||
|
|
||||||
|
//check count (empty or not?)
|
||||||
|
if q.numItems == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
pos := q.readPos % q.capacity |
||||||
|
return q.messages[pos] |
||||||
|
} |
||||||
|
|
||||||
|
func (q *messageSet) count() int { |
||||||
|
q.RLock() |
||||||
|
defer q.RUnlock() |
||||||
|
|
||||||
|
return q.numItems |
||||||
|
} |
||||||
|
|
||||||
|
func (q *messageSet) contains(m *BroadcastMessage) bool { |
||||||
|
q.RLock() |
||||||
|
defer q.RUnlock() |
||||||
|
|
||||||
|
beg := q.readPos |
||||||
|
end := q.writePos |
||||||
|
for { |
||||||
|
pos := beg % q.capacity |
||||||
|
if pos == end { |
||||||
|
break |
||||||
|
} |
||||||
|
|
||||||
|
//check message
|
||||||
|
m2 := q.messages[pos] |
||||||
|
eq := m.EqualTo(m2) |
||||||
|
if eq { |
||||||
|
return true |
||||||
|
} |
||||||
|
beg++ |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
//EqualTo return true if message is equal
|
||||||
|
func (m1 *BroadcastMessage) EqualTo(m2 *BroadcastMessage) bool { |
||||||
|
eq := m1.Timestamp == m2.Timestamp && |
||||||
|
m1.Duration == m2.Duration && |
||||||
|
m1.NBuzzer == m2.NBuzzer && |
||||||
|
m1.Expired == m2.Expired && |
||||||
|
m1.Message == m2.Message |
||||||
|
|
||||||
|
return eq |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
//NewDbMessageProvider create new provider
|
||||||
|
func NewDbMessageProvider(options *opt.Options) (*DbMessageProvider, error) { |
||||||
|
//make sure url ends with /
|
||||||
|
p := &DbMessageProvider{} |
||||||
|
|
||||||
|
//TODO init
|
||||||
|
return p, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DbMessageProvider) getFromDb(imei uint64) (*BroadcastMessage, error) { |
||||||
|
if !p.loadFromDb { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
//Get message from API
|
||||||
|
//TODO: can it handle many connections?
|
||||||
|
msg := &BroadcastMessage{} |
||||||
|
|
||||||
|
//alamat internal
|
||||||
|
imeiStr := fmt.Sprintf("%d", imei) |
||||||
|
url := p.messageAPI + imeiStr |
||||||
|
err := p.getJSON(url, msg) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
now := time.Now() |
||||||
|
if msg.Expired.Before(now) { |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
return msg, nil |
||||||
|
} |
||||||
|
func (p *DbMessageProvider) putToDb(message *UserMessage) error { |
||||||
|
if !p.saveToDb { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DbMessageProvider) updateDb(status *MessageStatus) error { |
||||||
|
url := p.reportAPI |
||||||
|
if p.appendIMEI { |
||||||
|
url += status.IMEI |
||||||
|
} |
||||||
|
err := p.putJSON(url, status) |
||||||
|
if err != nil { |
||||||
|
log.Printf("PUT request error: %v\n", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DbMessageProvider) getJSON(url string, target interface{}) error { |
||||||
|
r, err := p.serviceClient.Get(url) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer r.Body.Close() |
||||||
|
|
||||||
|
return json.NewDecoder(r.Body).Decode(target) |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DbMessageProvider) putJSON(url string, data interface{}) error { |
||||||
|
payload, err := json.Marshal(data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
reader := bytes.NewReader(payload) |
||||||
|
client := p.serviceClient |
||||||
|
req, err := http.NewRequest("PUT", url, reader) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
req.ContentLength = int64(len(payload)) |
||||||
|
req.Header.Set("Content-Type", "application/json") |
||||||
|
response, err := client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer response.Body.Close() |
||||||
|
|
||||||
|
//read content
|
||||||
|
result, err := ioutil.ReadAll(response.Body) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
str := string(result) |
||||||
|
if strings.Contains(str, "no") { |
||||||
|
return errors.New("Failed to PUT data to service: " + str) |
||||||
|
} |
||||||
|
|
||||||
|
//request code
|
||||||
|
code := response.StatusCode |
||||||
|
if code == http.StatusOK || code == http.StatusCreated { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
status := response.Status |
||||||
|
return errors.New("PUT error with status = " + status) |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
//Get return broadcast message for given IMEI
|
||||||
|
func (p *DbMessageProvider) Get(imei uint64) (*BroadcastMessage, error) { |
||||||
|
p.RLock() |
||||||
|
defer p.RUnlock() |
||||||
|
|
||||||
|
//get queue for this imei
|
||||||
|
queue, ok := p.queueMsg[imei] |
||||||
|
if !ok { |
||||||
|
return p.getFromDb(imei) |
||||||
|
} |
||||||
|
|
||||||
|
//get message
|
||||||
|
msg := queue.get() |
||||||
|
if msg == nil { |
||||||
|
return p.getFromDb(imei) |
||||||
|
} |
||||||
|
return msg, nil |
||||||
|
} |
||||||
|
|
||||||
|
//Put setup new messages
|
||||||
|
func (p *DbMessageProvider) Put(message *UserMessage) error { |
||||||
|
p.Lock() |
||||||
|
defer p.Unlock() |
||||||
|
|
||||||
|
now := ISOTime{time.Now()} |
||||||
|
for _, vid := range message.Vehicles { |
||||||
|
imei, err := strconv.ParseUint(vid, 10, 64) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Parse IMEI (%v) error: %v\n", vid, err) |
||||||
|
continue |
||||||
|
} |
||||||
|
bm := &BroadcastMessage{ |
||||||
|
Timestamp: now, |
||||||
|
Duration: message.Duration, |
||||||
|
NBuzzer: message.NBuzzer, |
||||||
|
Message: message.Message, |
||||||
|
Expired: message.Expired, |
||||||
|
} |
||||||
|
queue, ok := p.queueMsg[imei] |
||||||
|
if !ok { |
||||||
|
queue = newMessageSet(p.capacity) |
||||||
|
p.queueMsg[imei] = queue |
||||||
|
} |
||||||
|
|
||||||
|
ok = queue.putUnique(bm) |
||||||
|
if !ok { |
||||||
|
//log
|
||||||
|
log.Printf("Put message error-> %v:%v", imei, bm.Message) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//save to database
|
||||||
|
return p.putToDb(message) |
||||||
|
} |
||||||
|
|
||||||
|
//Update message status
|
||||||
|
func (p *DbMessageProvider) Update(status *MessageStatus) error { |
||||||
|
p.Lock() |
||||||
|
defer p.Unlock() |
||||||
|
|
||||||
|
imei, err := strconv.ParseUint(status.IMEI, 10, 64) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
//message delivered
|
||||||
|
if status.StatusCode == MessageDelivered || status.StatusCode == MessageError { |
||||||
|
//get message from "sent message" collection
|
||||||
|
sets, ok := p.sentMsg[imei] |
||||||
|
if !ok { |
||||||
|
return errors.New("delivered message not found in sets") |
||||||
|
} |
||||||
|
msg := sets.take() |
||||||
|
|
||||||
|
//if status is error, return message to broadcast queue
|
||||||
|
if msg != nil && status.StatusCode == MessageError { |
||||||
|
//return to sets
|
||||||
|
queue, ok := p.queueMsg[imei] |
||||||
|
if !ok { |
||||||
|
queue = newMessageSet(p.capacity) |
||||||
|
p.queueMsg[imei] = queue |
||||||
|
} |
||||||
|
queue.putUnique(msg) |
||||||
|
} |
||||||
|
} else if status.StatusCode == MessageSent { |
||||||
|
//message sent, get from queue
|
||||||
|
queue, ok := p.queueMsg[imei] |
||||||
|
if !ok { |
||||||
|
return errors.New("message not found in sets") |
||||||
|
} |
||||||
|
msg := queue.take() |
||||||
|
if msg != nil { |
||||||
|
//move to "sent message" collection
|
||||||
|
sets, ok := p.sentMsg[imei] |
||||||
|
if !ok { |
||||||
|
sets = newMessageSet(p.capacity) |
||||||
|
p.sentMsg[imei] = sets |
||||||
|
} |
||||||
|
sets.put(msg) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//update status in database
|
||||||
|
return p.updateDb(status) |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
package model |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
func TestQueue(t *testing.T) { |
||||||
|
queue := newMessageSet(20) |
||||||
|
finish := make(chan bool, 2) |
||||||
|
|
||||||
|
fnPush := func() { |
||||||
|
for k := 0; k < 30; k++ { |
||||||
|
msg := &BroadcastMessage{ |
||||||
|
Timestamp: ISOTime{time.Now()}, |
||||||
|
Message: fmt.Sprintf("Message %02d", k), |
||||||
|
} |
||||||
|
ok := queue.put(msg) |
||||||
|
if !ok { |
||||||
|
t.Logf("Push error (#n = %d)\n", queue.count()) |
||||||
|
time.Sleep(20) |
||||||
|
} |
||||||
|
} |
||||||
|
finish <- true |
||||||
|
} |
||||||
|
|
||||||
|
fnPop := func() { |
||||||
|
for k := 0; k < 50; k++ { |
||||||
|
msg := queue.take() |
||||||
|
if msg != nil { |
||||||
|
t.Logf("#Items %d, msg: %v -> %v", queue.count(), msg.Timestamp, msg.Message) |
||||||
|
} else { |
||||||
|
t.Logf("#num items %d", queue.count()) |
||||||
|
time.Sleep(1) |
||||||
|
} |
||||||
|
} |
||||||
|
finish <- true |
||||||
|
close(finish) |
||||||
|
} |
||||||
|
|
||||||
|
go fnPush() |
||||||
|
go fnPop() |
||||||
|
|
||||||
|
for ok := range finish { |
||||||
|
t.Logf("Finish %v", ok) |
||||||
|
} |
||||||
|
|
||||||
|
//pointer test
|
||||||
|
msg1 := &BroadcastMessage{ |
||||||
|
Timestamp: ISOTime{time.Now()}, |
||||||
|
} |
||||||
|
msg2 := msg1 |
||||||
|
msg3 := msg1 |
||||||
|
|
||||||
|
if msg2 == msg3 { |
||||||
|
t.Logf("Message is equal") |
||||||
|
} else { |
||||||
|
t.Logf("Message is not equal") |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,78 @@ |
|||||||
|
/* |
||||||
|
Package for server related model |
||||||
|
*/ |
||||||
|
|
||||||
|
package model |
||||||
|
|
||||||
|
const ( |
||||||
|
SERVER_ZMQ = "zmq" |
||||||
|
SERVER_NET = "net" |
||||||
|
|
||||||
|
CMD_START = "start" |
||||||
|
CMD_RESTART = "restart" |
||||||
|
CMD_STOP = "stop" |
||||||
|
|
||||||
|
DEV_RUPTELA = "RUPTELA" |
||||||
|
DEV_TELTONIKA = "TELTONIKA" |
||||||
|
|
||||||
|
MAP_SHARD_COUNT = 32 |
||||||
|
) |
||||||
|
|
||||||
|
/* |
||||||
|
REQ-REP pattern request. |
||||||
|
*/ |
||||||
|
type Request struct { |
||||||
|
Origin string |
||||||
|
Token string |
||||||
|
Type string |
||||||
|
Data string |
||||||
|
} |
||||||
|
|
||||||
|
type IoMapResponse struct { |
||||||
|
Descriptor DeviceDescriptor |
||||||
|
DevTags []DevTag |
||||||
|
StdTags []StdTag |
||||||
|
} |
||||||
|
|
||||||
|
type ServerConfig struct { |
||||||
|
Id int |
||||||
|
IP string |
||||||
|
Port int |
||||||
|
MaxSocket int |
||||||
|
Mode string |
||||||
|
ChanLimit uint32 |
||||||
|
Device string |
||||||
|
TimeoutMilis int |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Interface for service which provides several information. |
||||||
|
*/ |
||||||
|
type ServiceProvider interface { |
||||||
|
IsRegistered(imei uint64) bool |
||||||
|
GetDeviceIoMapping(imei uint64) (iom *DeviceIoMapping, err error) |
||||||
|
} |
||||||
|
|
||||||
|
//Device server interface
|
||||||
|
type DeviceServer interface { |
||||||
|
Start() |
||||||
|
Stop() |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Get device io mapping |
||||||
|
*/ |
||||||
|
func GetDeviceIoMapping(imei uint64) (iom *DeviceIoMapping, err error) { |
||||||
|
//-------------------------------------
|
||||||
|
//TODO get mapping from server/cache
|
||||||
|
iom = new(DeviceIoMapping) |
||||||
|
iom.Descriptor.Id = 10 |
||||||
|
iom.Descriptor.Model = "Pro3" |
||||||
|
iom.Descriptor.QoS = 1 |
||||||
|
iom.Descriptor.IMEI = imei |
||||||
|
if iom.IOMapping == nil { |
||||||
|
iom.IOMapping = make(IOMap) |
||||||
|
} |
||||||
|
|
||||||
|
return iom, nil |
||||||
|
} |
Binary file not shown.
@ -0,0 +1,573 @@ |
|||||||
|
/* |
||||||
|
Package for parsing devicePrs byte stream to struct. |
||||||
|
*/ |
||||||
|
|
||||||
|
package parser |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"encoding/hex" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"net/http" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"bytes" |
||||||
|
|
||||||
|
"io/ioutil" |
||||||
|
|
||||||
|
"github.com/ipsusila/opt" |
||||||
|
"github.com/confluentinc/confluent-kafka-go/kafka" |
||||||
|
"../model" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
statusSent = 1 |
||||||
|
statusDelivered = 2 |
||||||
|
statusError = -1 |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
timeLayout = "2006-01-02 15:04:05" |
||||||
|
) |
||||||
|
|
||||||
|
//NACK response, CRC = 565
|
||||||
|
var rNACK = []byte{0x00, 0x02, 0x64, 0x00, 0x02, 0x35} |
||||||
|
|
||||||
|
//ACK response, CRC = 5052
|
||||||
|
var rACK = []byte{0x00, 0x02, 0x64, 0x01, 0x13, 0xBC} |
||||||
|
|
||||||
|
//trasparent channel error
|
||||||
|
var rEmpty = []byte{0x00, 0x02, 0x64, 0x01, 0x13, 0xBC} |
||||||
|
|
||||||
|
//tch empty
|
||||||
|
var tEmpty = []byte{0, 4, 114, 1, 0, 0, 57, 239} |
||||||
|
|
||||||
|
//PortID for transparent channel
|
||||||
|
var PortID byte = 0x01 |
||||||
|
|
||||||
|
//auto-increment id
|
||||||
|
var iCounter int64 |
||||||
|
|
||||||
|
var sReplacer = strings.NewReplacer("\\", "\\\\", ":", "\\:", "$", "\\$") |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
|
//DevicePrsOptions store specific parser options
|
||||||
|
type DevicePrsOptions struct { |
||||||
|
messageAPI string |
||||||
|
reportAPI string |
||||||
|
serviceTimeout time.Duration |
||||||
|
appendIMEI bool |
||||||
|
insertQuery string |
||||||
|
serviceClient *http.Client |
||||||
|
tagOption *opt.Options |
||||||
|
BrokerServer string |
||||||
|
BrokerTopic string |
||||||
|
DeviceName string |
||||||
|
} |
||||||
|
|
||||||
|
//ISOTime encapsulate timestamp for JSON parsing
|
||||||
|
type ISOTime struct { |
||||||
|
time.Time |
||||||
|
} |
||||||
|
|
||||||
|
//BroadcastMessage store message information to be broadcasted
|
||||||
|
type BroadcastMessage struct { |
||||||
|
Timestamp ISOTime `json:"ts"` |
||||||
|
Duration int `json:"duration"` |
||||||
|
NBuzzer int `json:"nbuzzer"` |
||||||
|
Message string `json:"message"` |
||||||
|
Expired ISOTime `json:"expired"` |
||||||
|
} |
||||||
|
|
||||||
|
//MessageStatus for specific IMEI
|
||||||
|
type MessageStatus struct { |
||||||
|
IMEI string `json:"imei"` |
||||||
|
Status string `json:"status"` |
||||||
|
Timestamp string `json:"timestamp"` |
||||||
|
} |
||||||
|
|
||||||
|
//DevicePrsParser encapsulate parser configuration for DevicePrs device
|
||||||
|
type DevicePrsParser struct { |
||||||
|
options *DevicePrsOptions |
||||||
|
db *sql.DB |
||||||
|
MaxPacketSize int |
||||||
|
MinPacketSize int |
||||||
|
BufferSize int |
||||||
|
Data []byte |
||||||
|
IMEI uint64 |
||||||
|
Error error |
||||||
|
Command byte |
||||||
|
//Records *model.DeviceRecords
|
||||||
|
Records DeviceRecordHeader |
||||||
|
procdr *kafka.Producer |
||||||
|
} |
||||||
|
|
||||||
|
//UnmarshalJSON convert string to timestamp
|
||||||
|
func (tm *ISOTime) UnmarshalJSON(b []byte) (err error) { |
||||||
|
//TODO time other than local
|
||||||
|
s := strings.Trim(string(b), "\"") |
||||||
|
tm.Time, err = time.ParseInLocation(timeLayout, s, time.Local) |
||||||
|
if err != nil { |
||||||
|
tm.Time = time.Time{} |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
//GetMaxPacketSize return maximum packet size for this parser
|
||||||
|
func (p *DevicePrsParser) GetMaxPacketSize() int { |
||||||
|
return p.MaxPacketSize |
||||||
|
} |
||||||
|
|
||||||
|
//GetMinPacketSize return minimum valid packet size for this parser
|
||||||
|
func (p *DevicePrsParser) GetMinPacketSize() int { |
||||||
|
return p.MinPacketSize |
||||||
|
} |
||||||
|
|
||||||
|
//GetBufferSize return buffer size for reading data
|
||||||
|
func (p *DevicePrsParser) GetBufferSize() int { |
||||||
|
return p.BufferSize |
||||||
|
} |
||||||
|
|
||||||
|
//GetError return last error
|
||||||
|
func (p *DevicePrsParser) GetError() error { |
||||||
|
if p.Error != nil { |
||||||
|
return p.Error |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
//Payload return data associated stored in
|
||||||
|
func (p *DevicePrsParser) Payload() []byte { |
||||||
|
//Field Packet length IMEI Command ID Payload Data CRC16
|
||||||
|
//Size (bytes) 2 8 1 [1-1009] 2
|
||||||
|
n := len(p.Data) |
||||||
|
if n < p.MinPacketSize { |
||||||
|
return []byte{} |
||||||
|
} |
||||||
|
|
||||||
|
return p.Data[11:(n - 2)] |
||||||
|
} |
||||||
|
|
||||||
|
//GetStream return data stream
|
||||||
|
func (p *DevicePrsParser) GetStream() []byte { |
||||||
|
return p.Data |
||||||
|
} |
||||||
|
|
||||||
|
//GetIMEI return IMEI
|
||||||
|
func (p *DevicePrsParser) GetIMEI() uint64 { |
||||||
|
return p.IMEI |
||||||
|
} |
||||||
|
|
||||||
|
//GetCommand return command associated to this packet
|
||||||
|
func (p *DevicePrsParser) GetCommand() byte { |
||||||
|
return p.Command |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DevicePrsParser) saveToDatabase() error { |
||||||
|
imei := fmt.Sprintf("%d", p.IMEI) |
||||||
|
dInHex := hex.EncodeToString(p.Data) |
||||||
|
|
||||||
|
//TODO, make packet invalid if IMEI is not registered
|
||||||
|
db := p.db |
||||||
|
if db != nil { |
||||||
|
//TODO: save data to database
|
||||||
|
//`INSERT INTO "GPS_DATA"("IMEI", "DATA_LOG", "FLAG", "DATE_INS", "DESC", "GPS_CODE", "DATA_LEN")
|
||||||
|
//VALUES($1, $2, $3, $4, $5, $6, $7)`
|
||||||
|
now := time.Now().Local().Format(timeLayout) |
||||||
|
query := p.options.insertQuery |
||||||
|
result, err := db.Exec(query, imei, dInHex, false, now, "", p.options.DeviceName, len(p.Data)) |
||||||
|
if err != nil { |
||||||
|
log.Printf("INSERT ERROR: %s -> %s; %s: %s\n", err.Error(), query, imei, dInHex) |
||||||
|
return err |
||||||
|
} |
||||||
|
id, _ := result.LastInsertId() |
||||||
|
nrows, _ := result.RowsAffected() |
||||||
|
log.Printf("IMEI: %v, insert-id: %d, nrows: %d\n", imei, id, nrows) |
||||||
|
} else { |
||||||
|
//if database not defined, display packet in log
|
||||||
|
log.Printf("IMEI: %v\n Data: %v\n", imei, dInHex) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DevicePrsParser) sendToBroker() error { |
||||||
|
imei := fmt.Sprintf("%d", p.IMEI) |
||||||
|
|
||||||
|
//TODO, make packet invalid if IMEI is not registered
|
||||||
|
procdr := p.procdr |
||||||
|
if procdr != nil { |
||||||
|
dataJson, err := json.Marshal(p.Records) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error parse to JSON (IMEI: %v): %s\n", imei, err) |
||||||
|
} |
||||||
|
//log.Printf("JSON : %v\n",string(dataJson))
|
||||||
|
|
||||||
|
log.Printf("Number of records: %d (IMEI: %v)\n", p.Records.NumRec, imei) |
||||||
|
|
||||||
|
//broker
|
||||||
|
topic := p.options.BrokerTopic |
||||||
|
//producer channel
|
||||||
|
doneChan := make(chan bool) |
||||||
|
go func() { |
||||||
|
defer close(doneChan) |
||||||
|
for e := range procdr.Events() { |
||||||
|
switch ev := e.(type) { |
||||||
|
case *kafka.Message: |
||||||
|
m := ev |
||||||
|
if m.TopicPartition.Error != nil { |
||||||
|
log.Printf("Sent failed: %v (IMEI: %v)\n", m.TopicPartition.Error, imei) |
||||||
|
} else { |
||||||
|
log.Printf("Sent message to topic %s [%d] at offset %v (IMEI: %v)\n", |
||||||
|
*m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset, imei) |
||||||
|
} |
||||||
|
return |
||||||
|
|
||||||
|
default: |
||||||
|
log.Printf("Ignored event: %s (IMEI: %v)\n", ev,imei) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
value := string(dataJson) |
||||||
|
procdr.ProduceChannel() <- &kafka.Message{ |
||||||
|
TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
|
||||||
|
Value: []byte(value), |
||||||
|
Headers: []kafka.Header{{Key: imei, Value: []byte( p.Records.Tstamp.String() )}}, |
||||||
|
} |
||||||
|
// wait for delivery report goroutine to finish
|
||||||
|
_ = <-doneChan |
||||||
|
|
||||||
|
} else { |
||||||
|
//if database not defined, display packet in log
|
||||||
|
log.Printf("Message Broker not defined, IMEI: %v\n", imei) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DevicePrsParser) getJSON(url string, target interface{}) error { |
||||||
|
r, err := p.options.serviceClient.Get(url) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer r.Body.Close() |
||||||
|
|
||||||
|
return json.NewDecoder(r.Body).Decode(target) |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DevicePrsParser) putJSON(url string, data interface{}) error { |
||||||
|
if p.options == nil { |
||||||
|
return errors.New("PUT error, options not defined") |
||||||
|
} |
||||||
|
|
||||||
|
payload, err := json.Marshal(data) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
reader := bytes.NewReader(payload) |
||||||
|
client := p.options.serviceClient |
||||||
|
req, err := http.NewRequest("PUT", url, reader) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
req.ContentLength = int64(len(payload)) |
||||||
|
req.Header.Set("Content-Type", "application/json") |
||||||
|
response, err := client.Do(req) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer response.Body.Close() |
||||||
|
|
||||||
|
//read content
|
||||||
|
result, err := ioutil.ReadAll(response.Body) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
str := string(result) |
||||||
|
if strings.Contains(str, "no") { |
||||||
|
return errors.New("Failed to update message status: " + str) |
||||||
|
} |
||||||
|
|
||||||
|
//request code
|
||||||
|
code := response.StatusCode |
||||||
|
if code == http.StatusOK || code == http.StatusCreated { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
status := response.Status |
||||||
|
return errors.New("PUT error with status = " + status) |
||||||
|
} |
||||||
|
|
||||||
|
func (p *DevicePrsParser) askForBroadcastMessage() (string, error) { |
||||||
|
if p.options == nil { |
||||||
|
return "", errors.New("GET error, options not defined") |
||||||
|
} |
||||||
|
//Get message from API
|
||||||
|
//TODO: can it handle many connections?
|
||||||
|
msg := &BroadcastMessage{} |
||||||
|
|
||||||
|
//alamat internal
|
||||||
|
imei := fmt.Sprintf("%d", p.IMEI) |
||||||
|
url := p.options.messageAPI + imei |
||||||
|
err := p.getJSON(url, msg) |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
now := time.Now() |
||||||
|
if msg.Expired.Before(now) { |
||||||
|
return "", nil |
||||||
|
} |
||||||
|
|
||||||
|
//get buzzer count
|
||||||
|
nbeep := msg.NBuzzer |
||||||
|
if nbeep <= 0 { |
||||||
|
nbeep = 1 |
||||||
|
} else if nbeep > 9 { |
||||||
|
nbeep = 9 |
||||||
|
} |
||||||
|
|
||||||
|
dispSec := msg.Duration |
||||||
|
if dispSec <= 0 { |
||||||
|
dispSec = 1 |
||||||
|
} else if dispSec > 999 { |
||||||
|
dispSec = 999 |
||||||
|
} |
||||||
|
|
||||||
|
escapedMsg := sReplacer.Replace(msg.Message) |
||||||
|
pesan := fmt.Sprintf(":t%d%03d%v$", nbeep, dispSec, escapedMsg) |
||||||
|
|
||||||
|
//log message
|
||||||
|
log.Printf("IMEI = %d, message = %v\n", p.IMEI, pesan) |
||||||
|
|
||||||
|
//go update broadcast status (mark as "sent")
|
||||||
|
go p.updateBroadcastStatus(statusSent) |
||||||
|
|
||||||
|
//return the message
|
||||||
|
return pesan, nil |
||||||
|
} |
||||||
|
|
||||||
|
//Update broadcast status
|
||||||
|
func (p *DevicePrsParser) updateBroadcastStatus(status int) error { |
||||||
|
now := time.Now() |
||||||
|
report := &MessageStatus{ |
||||||
|
IMEI: fmt.Sprintf("%d", p.IMEI), |
||||||
|
Timestamp: now.Format(timeLayout), |
||||||
|
} |
||||||
|
switch status { |
||||||
|
case statusError: |
||||||
|
//Do nothing
|
||||||
|
log.Printf("IMEI=%d, Message status = Error\n", p.IMEI) |
||||||
|
case statusSent: |
||||||
|
report.Status = "sent" |
||||||
|
case statusDelivered: |
||||||
|
report.Status = "delivered" |
||||||
|
} |
||||||
|
|
||||||
|
if len(report.Status) > 0 && p.options != nil { |
||||||
|
url := p.options.reportAPI |
||||||
|
if p.options.appendIMEI { |
||||||
|
url += fmt.Sprintf("%d", p.IMEI) |
||||||
|
} |
||||||
|
err := p.putJSON(url, report) |
||||||
|
if err != nil { |
||||||
|
log.Printf("PUT request error: %v\n", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
log.Printf("IMEI=%v, Message status = %v\n", report.IMEI, report.Status) |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
//GetBroadcastMessage return message payload tobe broadcased to client
|
||||||
|
func (p *DevicePrsParser) GetBroadcastMessage() ([]byte, error) { |
||||||
|
pesan, err := p.askForBroadcastMessage() |
||||||
|
n := len(pesan) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
//construct message
|
||||||
|
npacket := n + 4 |
||||||
|
data := make([]byte, 8+n) |
||||||
|
data[0] = byte(npacket >> 8) |
||||||
|
data[1] = byte(npacket) |
||||||
|
data[2] = 0x72 |
||||||
|
data[3] = 0x01 //PORT 'B' (TODO: configurable)
|
||||||
|
data[4] = 0x00 |
||||||
|
data[5] = 0x00 |
||||||
|
|
||||||
|
//setup Payload
|
||||||
|
payload := []byte(pesan) |
||||||
|
ptr := data[6:] |
||||||
|
copy(ptr, payload) |
||||||
|
|
||||||
|
//calculate CRC
|
||||||
|
crc := crc16CCITT(data[2:(n + 6)]) |
||||||
|
data[n+6] = byte(crc >> 8) |
||||||
|
data[n+7] = byte(crc) |
||||||
|
|
||||||
|
return data, nil |
||||||
|
} |
||||||
|
|
||||||
|
//GetClientResponse return appropriate response to client according to packet
|
||||||
|
func (p *DevicePrsParser) GetClientResponse() []byte { |
||||||
|
//Get response to client
|
||||||
|
//TODO other response
|
||||||
|
switch int(p.Command) { |
||||||
|
case model.CMD_RUP_RECORD: |
||||||
|
if p.IMEI == 0 || p.Error != nil { |
||||||
|
return rNACK |
||||||
|
} |
||||||
|
return rACK |
||||||
|
case model.CMD_RUP_TRCH: |
||||||
|
if p.IMEI == 0 || p.Error != nil { |
||||||
|
return tEmpty |
||||||
|
} |
||||||
|
|
||||||
|
//parse incoming message
|
||||||
|
tch, err := parseTchStream(p.Data, p.MinPacketSize, p.MaxPacketSize) |
||||||
|
if err != nil { |
||||||
|
return tEmpty |
||||||
|
} |
||||||
|
|
||||||
|
//ceck command type
|
||||||
|
payload := string(tch.Data) |
||||||
|
if strings.HasPrefix(payload, "GMn") || |
||||||
|
strings.HasPrefix(payload, "GMi") || |
||||||
|
strings.HasPrefix(payload, "GMe") { |
||||||
|
|
||||||
|
//send message if initial, none or error
|
||||||
|
data, err := p.GetBroadcastMessage() |
||||||
|
if err != nil { |
||||||
|
p.Error = err |
||||||
|
return tEmpty |
||||||
|
} |
||||||
|
|
||||||
|
return data |
||||||
|
} else if strings.HasPrefix(payload, "GMo") { |
||||||
|
//ok, message delivered
|
||||||
|
go p.updateBroadcastStatus(statusDelivered) |
||||||
|
} |
||||||
|
|
||||||
|
//empty transparent channel
|
||||||
|
return tEmpty |
||||||
|
} |
||||||
|
|
||||||
|
//empty
|
||||||
|
return rEmpty |
||||||
|
} |
||||||
|
|
||||||
|
//GetRecords return parsed records
|
||||||
|
// func (p *DevicePrsParser) GetRecords() *model.DeviceRecords {
|
||||||
|
// if p.IMEI == 0 || p.Error != nil {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// //parse stream if not yet done
|
||||||
|
// if p.Records == nil {
|
||||||
|
// //Stream already verified when creating parser
|
||||||
|
// p.Records, p.Error = parseStream(p.Data, p.MinPacketSize, p.MaxPacketSize, true)
|
||||||
|
// if p.Error != nil {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return p.Records
|
||||||
|
// }
|
||||||
|
|
||||||
|
//ExecuteAsync perform task
|
||||||
|
func (p *DevicePrsParser) ExecuteAsync() bool { |
||||||
|
var status bool |
||||||
|
status = true |
||||||
|
//TODO identify according to command
|
||||||
|
cmd := p.GetCommand() |
||||||
|
switch cmd { |
||||||
|
case model.CMD_RUP_RECORD: |
||||||
|
err := p.saveToDatabase() |
||||||
|
if err != nil { |
||||||
|
status = false |
||||||
|
p.Error = err |
||||||
|
} |
||||||
|
|
||||||
|
err = p.sendToBroker() |
||||||
|
} |
||||||
|
|
||||||
|
return status |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
|
//NewDevicePrsOptions create options for devicePrs
|
||||||
|
func NewDevicePrsOptions(options *opt.Options) *DevicePrsOptions { |
||||||
|
svcOpt := options.Get(model.DEV_RUPTELA) |
||||||
|
|
||||||
|
defQuery := `INSERT INTO "GPS_DATA"("IMEI", "DATA_LOG", "FLAG", "DATE_INS", "DESC", "GPS_CODE", "DATA_LEN") |
||||||
|
VALUES($1, $2, $3, $4, $5, $6, $7)` |
||||||
|
insertQuery := svcOpt.GetString("insertQuery", defQuery) |
||||||
|
rupOpt := &DevicePrsOptions{ |
||||||
|
messageAPI: svcOpt.GetString("message", ""), |
||||||
|
reportAPI: svcOpt.GetString("report", ""), |
||||||
|
serviceTimeout: time.Duration(svcOpt.GetInt("serviceTimeout", 10)) * time.Second, |
||||||
|
appendIMEI: svcOpt.GetBool("appendIMEI", false), |
||||||
|
insertQuery: insertQuery, |
||||||
|
tagOption: options.Get("iotag"), |
||||||
|
BrokerServer: options.Get("messagebroker").GetString("brokerServer",""),
|
||||||
|
BrokerTopic: options.Get("messagebroker").GetString("brokerTopic",""), |
||||||
|
DeviceName: options.Get("server").GetString("device",""),
|
||||||
|
} |
||||||
|
//create client
|
||||||
|
rupOpt.serviceClient = &http.Client{Timeout: rupOpt.serviceTimeout} |
||||||
|
|
||||||
|
return rupOpt |
||||||
|
} |
||||||
|
|
||||||
|
//NewDevicePrs create empty parser
|
||||||
|
func NewDevicePrs() *DevicePrsParser { |
||||||
|
p := &DevicePrsParser{ |
||||||
|
MaxPacketSize: 2048, |
||||||
|
MinPacketSize: 14, |
||||||
|
BufferSize: 2048, |
||||||
|
} |
||||||
|
|
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
//NewDevicePrsParser create new parser. Maximum packet size is 1KB
|
||||||
|
func NewDevicePrsParser(options *DevicePrsOptions, db *sql.DB, data []byte, procdr *kafka.Producer) *DevicePrsParser { |
||||||
|
//allocate parser with maximum and minimum packet size
|
||||||
|
p := NewDevicePrs() |
||||||
|
p.options = options |
||||||
|
p.db = db |
||||||
|
p.procdr = procdr |
||||||
|
|
||||||
|
//verify stream
|
||||||
|
if data != nil { |
||||||
|
p.IMEI, p.Command, p.Records, p.Error = verifyStream(data, p.MinPacketSize, p.MaxPacketSize, options.tagOption) |
||||||
|
if p.Error == nil { |
||||||
|
p.Data = data |
||||||
|
} |
||||||
|
} else { |
||||||
|
p.Error = fmt.Errorf("Stream is empty") |
||||||
|
} |
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
//NewDevicePrsStringParser create new parser which accept HEX string.
|
||||||
|
//Maximum packet size is 1KB i.e. maximum string length is 2KB
|
||||||
|
func NewDevicePrsStringParser(options *DevicePrsOptions, db *sql.DB, data string, procdr *kafka.Producer) *DevicePrsParser { |
||||||
|
stream, err := hex.DecodeString(data) |
||||||
|
p := NewDevicePrsParser(options, db, stream,procdr) |
||||||
|
if err != nil { |
||||||
|
p.Error = err |
||||||
|
} |
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
@ -0,0 +1,445 @@ |
|||||||
|
/* |
||||||
|
Package for parsing devicePrs byte stream to struct. |
||||||
|
*/ |
||||||
|
|
||||||
|
package parser |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
//"encoding/hex"
|
||||||
|
"strconv" |
||||||
|
"sort" |
||||||
|
"github.com/ipsusila/opt" |
||||||
|
|
||||||
|
"../model" |
||||||
|
) |
||||||
|
|
||||||
|
type ruptelaTchHeader struct { |
||||||
|
Length uint16 |
||||||
|
IMEI uint64 |
||||||
|
CommandID byte |
||||||
|
PortID byte |
||||||
|
Reserved uint16 |
||||||
|
Timestamp uint32 |
||||||
|
} |
||||||
|
|
||||||
|
type DeviceRecordHeader struct { |
||||||
|
Imei uint64 |
||||||
|
Tstamp time.Time |
||||||
|
NumRec uint16 |
||||||
|
DeviceRecords []DeviceRecord |
||||||
|
} |
||||||
|
|
||||||
|
type DeviceRecord struct { |
||||||
|
Tstamp time.Time |
||||||
|
TstampInt int64 |
||||||
|
Imei uint64 |
||||||
|
Longitude float64 |
||||||
|
Latitude float64 |
||||||
|
Altitude uint16 |
||||||
|
Angle uint16 |
||||||
|
Satelites uint16 |
||||||
|
Speed uint16 |
||||||
|
TagDevices map[string]TagDevice |
||||||
|
} |
||||||
|
|
||||||
|
type TagDevice struct { |
||||||
|
TagName string |
||||||
|
TagId string |
||||||
|
TagDataType string |
||||||
|
TagVal string |
||||||
|
} |
||||||
|
|
||||||
|
// ByTstamp implements sort.Interface for []DeviceRecord based on
|
||||||
|
// the TstampInt field.
|
||||||
|
type ByTstamp []DeviceRecord |
||||||
|
|
||||||
|
func (a ByTstamp) Len() int { return len(a) } |
||||||
|
func (a ByTstamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
||||||
|
func (a ByTstamp) Less(i, j int) bool { return a[i].TstampInt < a[j].TstampInt } |
||||||
|
|
||||||
|
/* |
||||||
|
Verify that content of the byte stream match defined format |
||||||
|
*/ |
||||||
|
func verifyStream(data []byte, minSize int, maxSize int, tagOption *opt.Options) (imei uint64, cmd byte, rec DeviceRecordHeader, err error) { |
||||||
|
dlen := len(data) |
||||||
|
if dlen < minSize { |
||||||
|
return 0, 0,rec, fmt.Errorf("Packet size is too small (< %d)", dlen) |
||||||
|
} else if dlen > maxSize { |
||||||
|
return 0, 0,rec, fmt.Errorf("Packet size is greater than maximum allowed size (%d)", dlen) |
||||||
|
} |
||||||
|
|
||||||
|
//Extract packet size
|
||||||
|
sz := int(binary.BigEndian.Uint16(data[:2])) |
||||||
|
if sz != (dlen - 4) { |
||||||
|
return 0, 0,rec, fmt.Errorf("Size field mismatched (!%d)", dlen) |
||||||
|
} |
||||||
|
|
||||||
|
//extract IMEI
|
||||||
|
imei = binary.BigEndian.Uint64(data[2:10]) |
||||||
|
|
||||||
|
//extract and verify CRC
|
||||||
|
crc := binary.BigEndian.Uint16(data[dlen-2:]) |
||||||
|
ccrc := crc16CCITT(data[2 : dlen-2]) |
||||||
|
if crc != ccrc { |
||||||
|
return 0, 0,rec, fmt.Errorf("Invalid Ruptela packet (CRC-16 does not match)") |
||||||
|
} |
||||||
|
|
||||||
|
//parse data
|
||||||
|
rec = parseData(data,imei,tagOption) |
||||||
|
|
||||||
|
//extract command
|
||||||
|
cmd = data[10] |
||||||
|
|
||||||
|
return imei, cmd, rec, nil |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Parse byte stream and convert to standard record format |
||||||
|
*/ |
||||||
|
func parseData(data []byte, imei uint64, tagOption *opt.Options) (rec DeviceRecordHeader) { |
||||||
|
//allocate records
|
||||||
|
rec.Tstamp = time.Now() |
||||||
|
rec.Imei = imei |
||||||
|
|
||||||
|
tagOption_ := tagOption.GetObjectArray("tags") |
||||||
|
lengthTags := len(tagOption_) |
||||||
|
tags := make(map[string]TagDevice) |
||||||
|
for i:=0; i<lengthTags; i++ { |
||||||
|
var tag TagDevice |
||||||
|
tag.TagName = tagOption_[i].GetString("tagName","") |
||||||
|
tag.TagId = tagOption_[i].GetString("tagId","") |
||||||
|
tag.TagDataType = tagOption_[i].GetString("tagDataType","") |
||||||
|
tag.TagVal = "" |
||||||
|
|
||||||
|
tags[tag.TagId] = tag |
||||||
|
} |
||||||
|
|
||||||
|
//number of records
|
||||||
|
currByte := 12 |
||||||
|
plusByte := currByte + 1 |
||||||
|
numRec := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"numRec") |
||||||
|
//fmt.Printf("Number of records %d\n", numRec)
|
||||||
|
rec.NumRec = numRec |
||||||
|
deviceRecords := make([]DeviceRecord, 0) |
||||||
|
for j := 0; j < int(numRec); j++ { |
||||||
|
var deviceRecord DeviceRecord |
||||||
|
|
||||||
|
//imei
|
||||||
|
deviceRecord.Imei = imei |
||||||
|
|
||||||
|
//Timestamp
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 4 |
||||||
|
tstampInt := convBinaryToInt32(data[currByte:plusByte],8,"tstampInt") |
||||||
|
tstamp := time.Unix(int64(tstampInt), 0) |
||||||
|
//fmt.Printf("Timestamp %v\n",tstamp.String())
|
||||||
|
deviceRecord.TstampInt = int64(tstampInt) |
||||||
|
deviceRecord.Tstamp = tstamp |
||||||
|
|
||||||
|
//Priority
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 2 |
||||||
|
|
||||||
|
//Longitude
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 4 |
||||||
|
lonInt := convBinaryToInt32(data[currByte:plusByte],4,"lonInt") |
||||||
|
lon :=float64(lonInt)/10000000 |
||||||
|
//fmt.Printf("lon %f\n",lon)
|
||||||
|
deviceRecord.Longitude = lon |
||||||
|
|
||||||
|
//Latitude
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 4 |
||||||
|
latInt := convBinaryToInt32(data[currByte:plusByte],4,"latInt") |
||||||
|
lat :=float64(latInt)/10000000 |
||||||
|
//fmt.Printf("lat %f\n",lat)
|
||||||
|
deviceRecord.Latitude = lat |
||||||
|
|
||||||
|
//Altitude
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 2 |
||||||
|
alt := convBinaryToUint16(data[currByte:plusByte],2,"alt") |
||||||
|
//fmt.Printf("alt %d\n",alt)
|
||||||
|
deviceRecord.Altitude = alt/10 |
||||||
|
|
||||||
|
//Angle
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 2 |
||||||
|
angle := convBinaryToUint16(data[currByte:plusByte],2,"angle") |
||||||
|
//fmt.Printf("angle %d\n",angle)
|
||||||
|
deviceRecord.Angle = angle/100 |
||||||
|
|
||||||
|
//Satelite
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
satelites := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"satelites") |
||||||
|
//fmt.Printf("satelites %d\n",satelites)
|
||||||
|
deviceRecord.Satelites = satelites |
||||||
|
|
||||||
|
//Speed
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 2 |
||||||
|
speed := convBinaryToUint16(data[currByte:plusByte],2,"speed") |
||||||
|
//fmt.Printf("speed %d\n",speed)
|
||||||
|
deviceRecord.Speed = speed |
||||||
|
|
||||||
|
//HDOP - 09 (HEX) 9 (DEC) need to be divided by 10
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
|
||||||
|
//IO data cause record - 09 (HEX) 9 (DEC)
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//read 1 Byte I/O values
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//Total IO elements, which length is 1 Byte
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
totalOneByteIO := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"totalOneByteIO") |
||||||
|
//fmt.Printf("total IO1 %d\n",totalOneByteIO)
|
||||||
|
for i := 0; i < int(totalOneByteIO); i++ { |
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
ioID := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"io1ID") |
||||||
|
//fmt.Printf("io1ID %d\n",ioID)
|
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
ioVal := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"io1Val") |
||||||
|
//fmt.Printf("io1Val %d\n",ioVal)
|
||||||
|
|
||||||
|
tagDevice_ := tags[strconv.Itoa(int(ioID))] |
||||||
|
tagDevice_.TagId = strconv.Itoa(int(ioID)) |
||||||
|
tagDevice_.TagVal = strconv.Itoa(int(ioVal)) |
||||||
|
if tagDevice_.TagName != ""{ |
||||||
|
tags[strconv.Itoa(int(ioID))] = tagDevice_ |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//read 2 Byte I/O values
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//Total IO elements, which length is 2 Byte
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
totalTwoByteIO := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"totalTwoByteIO") |
||||||
|
//fmt.Printf("total IO2 %d\n",totalTwoByteIO)
|
||||||
|
for i := 0; i < int(totalTwoByteIO); i++ { |
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
ioID := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"io2ID") |
||||||
|
//fmt.Printf("io2ID %d\n",ioID)
|
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 2 |
||||||
|
ioVal := convBinaryToUint16(data[currByte:plusByte],2,"io2Val") |
||||||
|
//fmt.Printf("io2Val %d\n",ioVal)
|
||||||
|
|
||||||
|
tagDevice_ := tags[strconv.Itoa(int(ioID))] |
||||||
|
tagDevice_.TagId = strconv.Itoa(int(ioID)) |
||||||
|
tagDevice_.TagVal = strconv.Itoa(int(ioVal)) |
||||||
|
if tagDevice_.TagName != ""{ |
||||||
|
tags[strconv.Itoa(int(ioID))] = tagDevice_ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//read 4 Byte I/O values
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//Total IO elements, which length is 4 Byte
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
totalFourByteIO := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"totalFourByteIO") |
||||||
|
//fmt.Printf("total IO4 %d\n",totalFourByteIO)
|
||||||
|
for i := 0; i < int(totalFourByteIO); i++ { |
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
ioID := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"io4ID") |
||||||
|
//fmt.Printf("io4ID %d\n",ioID)
|
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 4 |
||||||
|
ioVal := convBinaryToInt32(data[currByte:plusByte],4,"io4Val") |
||||||
|
//fmt.Printf("io4Val %d\n",ioVal)
|
||||||
|
|
||||||
|
tagDevice_ := tags[strconv.Itoa(int(ioID))] |
||||||
|
tagDevice_.TagId = strconv.Itoa(int(ioID)) |
||||||
|
tagDevice_.TagVal = strconv.FormatInt(int64(ioVal), 10) |
||||||
|
if tagDevice_.TagName != ""{ |
||||||
|
tags[strconv.Itoa(int(ioID))] = tagDevice_ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//read 8 Byte I/O values
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
//Total IO elements, which length is 8 Byte
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
total8ByteIO := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"total8ByteIO") |
||||||
|
//fmt.Printf("total IO8 %d\n",total8ByteIO)
|
||||||
|
for i := 0; i < int(total8ByteIO); i++ { |
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 1 |
||||||
|
ioID := convBinaryToUint16(addOneByteInTwoByte(data[currByte:plusByte]),2,"io8ID") |
||||||
|
//fmt.Printf("io8ID %d\n",ioID)
|
||||||
|
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 8 |
||||||
|
ioVal := convBinaryToInt64(data[currByte:plusByte],8,"io8Val") |
||||||
|
//fmt.Printf("io8Val %d\n",ioVal)
|
||||||
|
|
||||||
|
tagDevice_ := tags[strconv.Itoa(int(ioID))] |
||||||
|
tagDevice_.TagId = strconv.Itoa(int(ioID)) |
||||||
|
tagDevice_.TagVal = strconv.FormatInt(int64(ioVal), 10) |
||||||
|
if tagDevice_.TagName != ""{ |
||||||
|
tags[strconv.Itoa(int(ioID))] = tagDevice_ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
deviceRecord.TagDevices = tags |
||||||
|
deviceRecords = append(deviceRecords, deviceRecord) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
//sort ascending by date
|
||||||
|
sort.Sort(ByTstamp(deviceRecords)) |
||||||
|
|
||||||
|
rec.DeviceRecords = deviceRecords |
||||||
|
|
||||||
|
//CRC-16
|
||||||
|
currByte = plusByte |
||||||
|
plusByte = currByte + 2 |
||||||
|
|
||||||
|
return rec |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert Binary to uint16 |
||||||
|
*/ |
||||||
|
func addOneByteInTwoByte(data []byte) (resultData []byte){ |
||||||
|
|
||||||
|
dataNew := make([]byte, 2) |
||||||
|
dataNew[0] = 0x0 |
||||||
|
copy(dataNew[1:], data) |
||||||
|
|
||||||
|
return dataNew |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert Binary to uint16 |
||||||
|
*/ |
||||||
|
func convBinaryToUint16(data []byte, byteLength int, desc string) (reslt uint16){ |
||||||
|
var dataInt uint16 |
||||||
|
dataNew := make([]byte, byteLength) |
||||||
|
copy(dataNew[0:], data) |
||||||
|
buf1 := bytes.NewReader(dataNew) |
||||||
|
err := binary.Read(buf1, binary.BigEndian, &dataInt) |
||||||
|
if err != nil { |
||||||
|
fmt.Printf("binary.Read failed %v :%v\n", desc, err) |
||||||
|
dataInt = 0 |
||||||
|
} |
||||||
|
return dataInt |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert Binary to int32 |
||||||
|
*/ |
||||||
|
func convBinaryToInt32(data []byte, byteLength int, desc string) (reslt int32){ |
||||||
|
var dataInt int32 |
||||||
|
dataNew := make([]byte, byteLength) |
||||||
|
copy(dataNew[0:], data) |
||||||
|
buf1 := bytes.NewReader(dataNew) |
||||||
|
err := binary.Read(buf1, binary.BigEndian, &dataInt) |
||||||
|
if err != nil { |
||||||
|
fmt.Printf("binary.Read failed %v :%v\n", desc, err) |
||||||
|
dataInt = 0 |
||||||
|
} |
||||||
|
return dataInt |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Convert Binary to int64 |
||||||
|
*/ |
||||||
|
func convBinaryToInt64(data []byte, byteLength int, desc string) (reslt int64){ |
||||||
|
var dataInt int64 |
||||||
|
dataNew := make([]byte, byteLength) |
||||||
|
copy(dataNew[0:], data) |
||||||
|
buf1 := bytes.NewReader(dataNew) |
||||||
|
err := binary.Read(buf1, binary.BigEndian, &dataInt) |
||||||
|
if err != nil { |
||||||
|
fmt.Printf("binary.Read failed %v :%v\n", desc, err) |
||||||
|
dataInt = 0 |
||||||
|
} |
||||||
|
return dataInt |
||||||
|
} |
||||||
|
|
||||||
|
func parseTchStream(data []byte, minSize int, maxSize int) (*model.DisplayRequest, error) { |
||||||
|
rec := &model.DisplayRequest{} |
||||||
|
|
||||||
|
//check length
|
||||||
|
dataLen := len(data) |
||||||
|
if dataLen < minSize { |
||||||
|
return rec, fmt.Errorf("Packet size is too small (< %d)", dataLen) |
||||||
|
} |
||||||
|
if dataLen > maxSize { |
||||||
|
return rec, fmt.Errorf("Packet size is greater than maximum allowed size (%d)", dataLen) |
||||||
|
} |
||||||
|
|
||||||
|
//create buffer
|
||||||
|
buf := bytes.NewBuffer(data) |
||||||
|
|
||||||
|
//read packet header part
|
||||||
|
pktHdr := &ruptelaTchHeader{} |
||||||
|
err := binary.Read(buf, binary.BigEndian, pktHdr) |
||||||
|
if err != nil { |
||||||
|
return rec, fmt.Errorf("Failed to read packet header (%v)", err) |
||||||
|
} |
||||||
|
//2+8+1+1+2+4 (len, imei, cmd, port, reserved, timestamp)
|
||||||
|
idx := 2 + 8 + 1 + 1 + 2 + 4 |
||||||
|
|
||||||
|
rec.IMEI = pktHdr.IMEI |
||||||
|
rec.PortID = pktHdr.PortID |
||||||
|
rec.Timestamp = time.Unix(int64(pktHdr.Timestamp), 0) |
||||||
|
rec.Data = data[idx:(dataLen - 2)] |
||||||
|
|
||||||
|
return rec, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Calculate CRC16 with CCITT algorithm
|
||||||
|
// Based on C/C++ code in DevicePrs protocol documents
|
||||||
|
// CRC calculation omits Length(2-bytes) and CRC itself (2-bytes)
|
||||||
|
func crc16CCITT(data []byte) uint16 { |
||||||
|
//--------------------------------------------------------------------
|
||||||
|
var ucBit, ucCarry uint16 |
||||||
|
//--------------------------------------------------------------------
|
||||||
|
var usPoly uint16 = 0x8408 //reversed 0x1021
|
||||||
|
var usCRC uint16 //initialized as zero
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
for _, d := range data { |
||||||
|
usCRC ^= uint16(d) |
||||||
|
for ucBit = 0; ucBit < 8; ucBit++ { |
||||||
|
ucCarry = usCRC & 1 |
||||||
|
usCRC >>= 1 |
||||||
|
if ucCarry != 0 { |
||||||
|
usCRC ^= usPoly |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
//--------------------------------------------------------------------
|
||||||
|
return usCRC |
||||||
|
//--------------------------------------------------------------------
|
||||||
|
} |
@ -0,0 +1,409 @@ |
|||||||
|
package parser |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"encoding/gob" |
||||||
|
"encoding/hex" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/ipsusila/opt" |
||||||
|
|
||||||
|
"time" |
||||||
|
|
||||||
|
"../model" |
||||||
|
) |
||||||
|
|
||||||
|
func TestCRC2(t *testing.T) { |
||||||
|
pesan := ":t2005Custom Message\\: Hello Server$" |
||||||
|
n := len(pesan) |
||||||
|
npacket := n + 4 |
||||||
|
data := make([]byte, 8+n) |
||||||
|
data[0] = byte(npacket >> 8) |
||||||
|
data[1] = byte(npacket) |
||||||
|
data[2] = 0x72 |
||||||
|
data[3] = 0x01 //PORT 'B'
|
||||||
|
data[4] = 0x00 |
||||||
|
data[5] = 0x00 |
||||||
|
|
||||||
|
payload := []byte(pesan) |
||||||
|
p := data[6:] |
||||||
|
copy(p, payload) |
||||||
|
|
||||||
|
crc := crc16CCITT(data[2:(n + 6)]) |
||||||
|
data[n+6] = byte(crc >> 8) |
||||||
|
data[n+7] = byte(crc) |
||||||
|
|
||||||
|
fmt.Printf("Data: %X", data) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCRC(t *testing.T) { |
||||||
|
//data := []byte{0x00, 0x02, 0x64, 0x01, 0x13, 0xBC}
|
||||||
|
data := []byte{0x00, 0x05, 0x72, 0x01, 0x00, 0x00, 0x66, 0xC5, 0x44} |
||||||
|
n1 := len(data) |
||||||
|
|
||||||
|
//ini OK
|
||||||
|
d1 := data[2 : n1-2] |
||||||
|
fmt.Printf("Data: %X\n", d1) |
||||||
|
crc1 := crc16CCITT(d1) |
||||||
|
|
||||||
|
//salah
|
||||||
|
d2 := data[0 : n1-2] |
||||||
|
fmt.Printf("Data: %X\n", d2) |
||||||
|
crc2 := crc16CCITT(d2) |
||||||
|
|
||||||
|
//salah
|
||||||
|
d3 := data //[0 : n1-2]
|
||||||
|
fmt.Printf("Data: %X\n", d3) |
||||||
|
crc3 := crc16CCITT(d3) |
||||||
|
|
||||||
|
fmt.Printf("#ACK CRC=%X, %X %X\n", crc1, byte(crc1>>8), byte(crc1)) |
||||||
|
fmt.Printf("#ACK CRC=%X\n", crc2) |
||||||
|
fmt.Printf("#ACK CRC=%X\n", crc3) |
||||||
|
} |
||||||
|
|
||||||
|
// func xxTestParser(t *testing.T) {
|
||||||
|
|
||||||
|
// //Parse data from DevicePrs manual
|
||||||
|
// var p model.Parser = NewDevicePrsStringParser(nil, nil,
|
||||||
|
// "033500000C076B5C208F01011E5268CEF20000196E3A3A0AEF3E934F3E2D780000000007000000005268CEFD0000196E3A3A0AEF3E934F3"+
|
||||||
|
// "E2D780000000007000000005268CF080000196E3A3A0AEF3E934F3E2D780000000007000000005268CF130000196E3A3A0AEF3E934F3E2D7"+
|
||||||
|
// "80000000007000000005268CF1E0000196E3A3A0AEF3E934F3E2D780000000007000000005268CF290000196E3A3A0AEF3E934F3E2D78000"+
|
||||||
|
// "0000007000000005268CF340000196E3A3A0AEF3E934F3E2D780000000007000000005268CF3F0000196E3A3A0AEF3E934F3E2D780000000"+
|
||||||
|
// "007000000005268CF4A0000196E3A3A0AEF3E934F3E2D780000000007000000005268CF550000196E3A3A0AEF3E934F3E2D7800000000070"+
|
||||||
|
// "00000005268CF600000196E3A3A0AEF3E934F3E2D780000000007000000005268CF6B0000196E3A3A0AEF3E934F3E2D78000000000700000"+
|
||||||
|
// "0005268CF730000196E36630AEF42CE4F6D0BF40400022208000000005268CF7E0000196E36B60AEF42BE4F6D0BF400000000070000000052"+
|
||||||
|
// "68CF890000196E36B60AEF42BE4F6D0BF40000000007000000005268CF940000196E36B60AEF42BE4F6D0BF40000000007000000005268CF"+
|
||||||
|
// "9F0000196E36B60AEF42BE4F6D0BF40000000007000000005268CFAA0000196E36B60AEF42BE4F6D0BF40000000007000000005268CFB50"+
|
||||||
|
// "000196E36B60AEF42BE4F6D0BF40000000007000000005268CFC00000196E36B60AEF42BE4F6D0BF40000000007000000005268CFCB00001"+
|
||||||
|
// "96E36B60AEF42BE4F6D0BF40000000007000000005268CFD60000196E36B60AEF42BE4F6D0BF40000000007000000005268CFD70000196E"+
|
||||||
|
// "3C710AEF5EFF4F690BF40400011708000000005268CFE20000196E3B980AEF601A4F690BF40000000007000000005268CFED0000196E3B980"+
|
||||||
|
// "AEF601A4F690BF40000000007000000005268CFF80000196E3B980AEF601A4F690BF40000000007000000005268D0030000196E3B980AEF60"+
|
||||||
|
// "1A4F690BF40000000007000000005268D00E0000196E3B980AEF601A4F690BF40000000007000000005268D0190000196E3B980AEF601A4F6"+
|
||||||
|
// "90BF40000000007000000005268D0240000196E3B980AEF601A4F690BF400000000070000000046E2")
|
||||||
|
|
||||||
|
// //convert string to binary HEX
|
||||||
|
// if p.GetError() != nil {
|
||||||
|
// t.Fatal(p.GetError())
|
||||||
|
// }
|
||||||
|
// pkt := p.GetRecords()
|
||||||
|
|
||||||
|
// //display packet as JSON
|
||||||
|
// b, err := json.Marshal(pkt)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal(err)
|
||||||
|
// }
|
||||||
|
// fmt.Println(string(b))
|
||||||
|
// // ---------------------------------------------------------
|
||||||
|
|
||||||
|
// data2, err2 := hex.DecodeString(
|
||||||
|
// "007900000b1a2a5585c30100024e9c036900000f101733208ff45e07b31b570a001009090605011b1a020003001c01ad01021d338e1600000" +
|
||||||
|
// "2960000601a41014bc16d004e9c038400000f104fdf20900d20075103b00a001308090605011b1a020003001c01ad01021d33b11600000296" +
|
||||||
|
// "0000601a41014bc1ea0028f9")
|
||||||
|
|
||||||
|
// //convert string to binary HEX
|
||||||
|
// if err2 != nil {
|
||||||
|
// t.Fatal(err2)
|
||||||
|
// }
|
||||||
|
// p = NewDevicePrsParser(nil, nil, data2)
|
||||||
|
// if p.GetError() != nil {
|
||||||
|
// t.Fatal(p.GetError())
|
||||||
|
// }
|
||||||
|
// pkt = p.GetRecords()
|
||||||
|
|
||||||
|
// //get response CRC
|
||||||
|
// rep := p.GetClientResponse()
|
||||||
|
// crc := crc16CCITT(rep[2 : len(rep)-2])
|
||||||
|
// fmt.Println("#ACK CRC=", crc)
|
||||||
|
|
||||||
|
// //display packet as JSON
|
||||||
|
// b, err = json.Marshal(pkt)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal(err)
|
||||||
|
// }
|
||||||
|
// origPkt := string(b)
|
||||||
|
// fmt.Println(origPkt)
|
||||||
|
|
||||||
|
// //test for map
|
||||||
|
// m := make(map[byte]int)
|
||||||
|
// m[0] = 10
|
||||||
|
// m[11] = 20
|
||||||
|
// m[2] = 30
|
||||||
|
|
||||||
|
// fmt.Println(m)
|
||||||
|
|
||||||
|
// ioM := new(model.IoMapResponse)
|
||||||
|
// ioM.Descriptor.IMEI = 10
|
||||||
|
// ioM.Descriptor.Id = 11
|
||||||
|
// ioM.Descriptor.Model = "ABC"
|
||||||
|
// ioM.Descriptor.QoS = 4
|
||||||
|
// ioM.DevTags = []model.DevTag{0, 1, 2, 3}
|
||||||
|
// ioM.StdTags = []model.StdTag{10, 11, 2, 4}
|
||||||
|
|
||||||
|
// b, err = json.Marshal(ioM)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal(err)
|
||||||
|
// }
|
||||||
|
// jstr := string(b)
|
||||||
|
// fmt.Println(jstr)
|
||||||
|
|
||||||
|
// var io2 model.IoMapResponse
|
||||||
|
// err = json.Unmarshal([]byte(jstr), &io2)
|
||||||
|
// fmt.Println(io2)
|
||||||
|
|
||||||
|
// var b1 byte = 200
|
||||||
|
// x := int8(b1)
|
||||||
|
// y := byte(x)
|
||||||
|
// fmt.Printf("%d -> %d -> %d\n", b1, x, y)
|
||||||
|
|
||||||
|
// by := data2[len(data2)-2:]
|
||||||
|
// fmt.Printf("Byte = %+v, %d\n", by, binary.BigEndian.Uint16(by))
|
||||||
|
|
||||||
|
// p = NewDevicePrsParser(nil, nil, []byte{})
|
||||||
|
// fmt.Println("Parser ", p)
|
||||||
|
|
||||||
|
// p = NewDevicePrsParser(nil, nil, nil)
|
||||||
|
// fmt.Println("Parser ", p)
|
||||||
|
// if p.GetError() != nil {
|
||||||
|
// fmt.Println(p.GetError())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// rep = p.GetClientResponse()
|
||||||
|
// crc = crc16CCITT(rep[2 : len(rep)-2])
|
||||||
|
// fmt.Println("#NACK CRC=", crc)
|
||||||
|
|
||||||
|
// imei, cmd, _ := verifyStream(data2, p.GetMinPacketSize(), p.GetMaxPacketSize())
|
||||||
|
// fmt.Printf("IMEI = %X, cmd = %d\n", imei, cmd)
|
||||||
|
|
||||||
|
// //test encoding/decoding
|
||||||
|
// var network bytes.Buffer // Stand-in for a network connection
|
||||||
|
// enc := gob.NewEncoder(&network) // Will write to network.
|
||||||
|
// dec := gob.NewDecoder(&network) // Will read from network.
|
||||||
|
|
||||||
|
// // Encode (send) some values.
|
||||||
|
// err = enc.Encode(pkt)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal("encode error:", err)
|
||||||
|
// }
|
||||||
|
// //fmt.Printf("Buffer = %+v\n", network)
|
||||||
|
|
||||||
|
// // Decode (receive) and print the values.
|
||||||
|
// var dpkt model.DeviceRecords
|
||||||
|
// err = dec.Decode(&dpkt)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal("decode error 1:", err)
|
||||||
|
// }
|
||||||
|
// b, err = json.Marshal(&dpkt)
|
||||||
|
// decPkt := string(b)
|
||||||
|
// fmt.Printf("%+v\n", decPkt)
|
||||||
|
|
||||||
|
// if origPkt != decPkt {
|
||||||
|
// t.Fatal("Encode/Decode records doesnot match.")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// //test original converter
|
||||||
|
// conv := model.RecordsConverter{}
|
||||||
|
// stream, err := conv.ToStream(pkt)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal("To stream error: ", err)
|
||||||
|
// }
|
||||||
|
// fmt.Printf("Stream length = %d bytes, orig = %d, json = %d\n",
|
||||||
|
// len(stream), len(data2), len(origPkt))
|
||||||
|
// //fmt.Printf("Stream = %+v\n", string(stream))
|
||||||
|
|
||||||
|
// pkt2, err := conv.FromStream(stream)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatal("From stream error: ", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// b, err = json.Marshal(&pkt2)
|
||||||
|
// decPkt2 := string(b)
|
||||||
|
// if origPkt != decPkt2 {
|
||||||
|
// t.Fatal("Encode/Decode records doesnot match.")
|
||||||
|
// }
|
||||||
|
// fmt.Printf("%+v\n", decPkt2)
|
||||||
|
|
||||||
|
// nack := []byte{0x00, 0x02, 0x64, 0x00, 0x02, 0x35}
|
||||||
|
// crc1 := crc16CCITT(nack[2 : len(nack)-2])
|
||||||
|
// fmt.Printf("CRC = %x\n", crc1)
|
||||||
|
|
||||||
|
// tsch := []byte{0x00, 0x05, 0x72, 0x01, 0x00, 0x00, 0x66, 0x19, 0xF0}
|
||||||
|
// crc1 = crc16CCITT(tsch[2 : len(tsch)-2])
|
||||||
|
// fmt.Printf("CRC = %x\n", crc1)
|
||||||
|
|
||||||
|
// str := hex.EncodeToString(tsch)
|
||||||
|
// fmt.Printf("Hex = %+v\n", str)
|
||||||
|
|
||||||
|
// data3, _ := hex.DecodeString(
|
||||||
|
// "0011000315A07F44865A0E01000053EA01DF65AD6D")
|
||||||
|
// crc1 = crc16CCITT(data3[2 : len(data3)-2])
|
||||||
|
// fmt.Printf("CRC = %x, %d, %d\n", crc1, len(data3), len(data2))
|
||||||
|
// }
|
||||||
|
|
||||||
|
func TestTch(t *testing.T) { |
||||||
|
str := "0011000315A07F44865A0E01000053EA01DF65AD6D" |
||||||
|
data, err := hex.DecodeString(str) |
||||||
|
|
||||||
|
crc1 := crc16CCITT(data[2 : len(data)-2]) |
||||||
|
t.Logf("CRC = %X", crc1) |
||||||
|
|
||||||
|
p := NewDevicePrsParser(nil, nil, data) |
||||||
|
if p.Error != nil { |
||||||
|
t.Errorf("Parser error: %v", p.Error) |
||||||
|
t.FailNow() |
||||||
|
} |
||||||
|
|
||||||
|
t.Logf("IMEI=%v, command=%d", p.IMEI, p.Command) |
||||||
|
t.Logf("Struct = %+v", p) |
||||||
|
|
||||||
|
tch, err := parseTchStream(p.Data, p.MinPacketSize, p.MaxPacketSize) |
||||||
|
if err != nil { |
||||||
|
t.Errorf("Parse TCH error: %v", err) |
||||||
|
t.FailNow() |
||||||
|
} |
||||||
|
|
||||||
|
t.Logf("TCH = %+v\n", tch) |
||||||
|
t.Logf("Data = %v", string(tch.Data)) |
||||||
|
|
||||||
|
//test
|
||||||
|
pesan := "" |
||||||
|
n := len(pesan) |
||||||
|
|
||||||
|
//construct message
|
||||||
|
npacket := n + 4 |
||||||
|
data = make([]byte, 8+n) |
||||||
|
data[0] = byte(npacket >> 8) |
||||||
|
data[1] = byte(npacket) |
||||||
|
data[2] = 0x72 |
||||||
|
data[3] = 0x01 //PORT 'B' (TODO: configurable)
|
||||||
|
data[4] = 0x00 |
||||||
|
data[5] = 0x00 |
||||||
|
|
||||||
|
//setup Payload
|
||||||
|
payload := []byte(pesan) |
||||||
|
ptr := data[6:] |
||||||
|
copy(ptr, payload) |
||||||
|
|
||||||
|
//calculate CRC
|
||||||
|
crc := crc16CCITT(data[2:(n + 6)]) |
||||||
|
data[n+6] = byte(crc >> 8) |
||||||
|
data[n+7] = byte(crc) |
||||||
|
|
||||||
|
t.Logf("Data = %v\n", data) |
||||||
|
} |
||||||
|
|
||||||
|
func getJSON(url string, target interface{}) error { |
||||||
|
restClient := &http.Client{Timeout: 10 * time.Second} |
||||||
|
r, err := restClient.Get(url) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
defer r.Body.Close() |
||||||
|
|
||||||
|
return json.NewDecoder(r.Body).Decode(target) |
||||||
|
} |
||||||
|
|
||||||
|
func TestJSON(t *testing.T) { |
||||||
|
msg := &BroadcastMessage{} |
||||||
|
imei := "868324028509698" |
||||||
|
url := "http://202.47.70.196/oslog/api/BroadcastPrivate/getbroadcastmessage/" + imei |
||||||
|
err := getJSON(url, msg) |
||||||
|
if err != nil { |
||||||
|
t.Errorf("Error get JSON: %v", err) |
||||||
|
t.FailNow() |
||||||
|
} |
||||||
|
|
||||||
|
t.Logf("JSON = %+v", msg) |
||||||
|
} |
||||||
|
|
||||||
|
func TestOptions(t *testing.T) { |
||||||
|
cfgText := ` |
||||||
|
{ |
||||||
|
server: { |
||||||
|
id: conoco01 |
||||||
|
listenAddr: ":8081" |
||||||
|
acceptTimeout: 10 # timeout (dalam detik) |
||||||
|
writeTimeout: 10 # timeout (dalam detik) |
||||||
|
maxReceive: 50 |
||||||
|
maxSend: 50 |
||||||
|
device: ruptela |
||||||
|
} |
||||||
|
|
||||||
|
ruptela: { |
||||||
|
message: "http://localhost:8081/api/" |
||||||
|
report: "http://localhost:8081/reportAPI" |
||||||
|
appendIMEI: false |
||||||
|
serviceTimeout: 10 # REST Service timeout (dalam detik) |
||||||
|
} |
||||||
|
|
||||||
|
gpsdata: { |
||||||
|
storage: sql |
||||||
|
sql: { |
||||||
|
driver: postgres |
||||||
|
connection: "user=isi-user password=isi-password dbname=OSLOGREC_MTRACK host=127.0.0.1 port=5432 connect_timeout=30 sslmode=disable" |
||||||
|
maxIdle: 10 #Max jumlah koneksi idle |
||||||
|
maxOpen: 10 #Max jumlah koneksi open |
||||||
|
maxLifetime: 60 #Maximum lifetime (dalam detik) |
||||||
|
insertQuery: INSERT INTO "GPS_DATA"("IMEI", "DATA_LOG", "FLAG", "DATE_INS", "DESC", "GPS_CODE", "DATA_LEN") VALUES($1, $2, $3, $4, $5, $6, $7) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
log: { |
||||||
|
#Known types: console, file, sql |
||||||
|
type: console, file, sql |
||||||
|
console: { |
||||||
|
# use default options |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#for file (uncomment type) |
||||||
|
# type: file |
||||||
|
file: { |
||||||
|
name: ./applog.log |
||||||
|
append: true |
||||||
|
} |
||||||
|
|
||||||
|
#for sql (uncomment type) |
||||||
|
# SQLite -> driver: sqlite3 |
||||||
|
# PostgreSQL -> driver: postgres |
||||||
|
# type: sql |
||||||
|
sql: { |
||||||
|
driver: sqlite3 |
||||||
|
connection: ./applog.sqlite |
||||||
|
maxIdle: 10 #Max jumlah koneksi idle |
||||||
|
maxOpen: 10 #Max jumlah koneksi open |
||||||
|
maxLifetime: 60 #Maximum lifetime (dalam detik) |
||||||
|
createQuery: CREATE TABLE applog(id INTEGER PRIMARY KEY AUTOINCREMENT, ts DATETIME, app VARCHAR(100), content TEXT) |
||||||
|
insertQuery: INSERT INTO applog(ts, app, content) VALUES(?, ?, ?) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
options, err := opt.FromText(cfgText, "hjson") |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
t.FailNow() |
||||||
|
} |
||||||
|
|
||||||
|
t.Logf("%+v", options) |
||||||
|
|
||||||
|
now := time.Now().Format(timeLayout) |
||||||
|
nowLocal := time.Now().Local().Format(timeLayout) |
||||||
|
t.Logf("Now= %v, local= %v\n", now, nowLocal) |
||||||
|
|
||||||
|
//test container
|
||||||
|
/* |
||||||
|
c1, key := options.GetContainer("server") |
||||||
|
t.Logf("Key = %s, Server = %+v", key, c1) |
||||||
|
|
||||||
|
c2, key := options.GetContainer("server.id") |
||||||
|
t.Logf("Key = %s, Server.id = %+v", key, c2) |
||||||
|
|
||||||
|
c3, key := options.GetContainer("log.sql") |
||||||
|
t.Logf("Key = %s, Log.sql = %+v", key, c3) |
||||||
|
*/ |
||||||
|
} |
Binary file not shown.
@ -0,0 +1,325 @@ |
|||||||
|
//
|
||||||
|
// DevicePrs Devie Server based on Go net package
|
||||||
|
// specify Mode=net in model.ServerConfig
|
||||||
|
//
|
||||||
|
|
||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"encoding/binary" |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
"syscall" |
||||||
|
"time" |
||||||
|
"encoding/hex" |
||||||
|
"sync" |
||||||
|
"math" |
||||||
|
|
||||||
|
"github.com/gansidui/gotcp" |
||||||
|
"github.com/ipsusila/opt" |
||||||
|
"github.com/confluentinc/confluent-kafka-go/kafka" |
||||||
|
//"../model"
|
||||||
|
"../parser" |
||||||
|
) |
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
var serverId = "" |
||||||
|
|
||||||
|
type ConnectionDevice struct { |
||||||
|
imei uint64 |
||||||
|
data []byte |
||||||
|
timeProcess time.Time |
||||||
|
} |
||||||
|
type MapConnection struct { |
||||||
|
addr map[string]ConnectionDevice |
||||||
|
mux sync.Mutex |
||||||
|
} |
||||||
|
func (c *MapConnection) AddUpdt(key string,connectionDevice ConnectionDevice) { |
||||||
|
c.mux.Lock() |
||||||
|
// Lock so only one goroutine at a time can access the map c.v.
|
||||||
|
c.addr[key] = connectionDevice |
||||||
|
c.mux.Unlock() |
||||||
|
} |
||||||
|
func (c *MapConnection) Del(key string) { |
||||||
|
c.mux.Lock() |
||||||
|
// Lock so only one goroutine at a time can access the map c.v.
|
||||||
|
delete(c.addr, key) |
||||||
|
c.mux.Unlock() |
||||||
|
} |
||||||
|
func (c *MapConnection) Value(key string) ConnectionDevice { |
||||||
|
c.mux.Lock() |
||||||
|
// Lock so only one goroutine at a time can access the map c.v.
|
||||||
|
defer c.mux.Unlock() |
||||||
|
return c.addr[key] |
||||||
|
} |
||||||
|
var connectionDevices = MapConnection{addr: make(map[string]ConnectionDevice)} |
||||||
|
|
||||||
|
//DevicePrsServerPacket holds packet information
|
||||||
|
type DevicePrsServerPacket struct { |
||||||
|
buff []byte |
||||||
|
} |
||||||
|
|
||||||
|
//NewDevicePrsServerPacket create new packet
|
||||||
|
func NewDevicePrsServerPacket(data []byte) *DevicePrsServerPacket { |
||||||
|
pkt := new(DevicePrsServerPacket) |
||||||
|
pkt.buff = data |
||||||
|
|
||||||
|
return pkt |
||||||
|
} |
||||||
|
|
||||||
|
//Serialize packet
|
||||||
|
func (p *DevicePrsServerPacket) Serialize() []byte { |
||||||
|
return p.buff |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
//DevicePrsServerNet info
|
||||||
|
type DevicePrsServerNet struct { |
||||||
|
Options *opt.Options |
||||||
|
Server *gotcp.Server |
||||||
|
Callback *DevicePrsServerCallback |
||||||
|
} |
||||||
|
|
||||||
|
//Stop tcp server
|
||||||
|
func (rup *DevicePrsServerNet) Stop() { |
||||||
|
rup.Server.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
//Start the server
|
||||||
|
func (rup *DevicePrsServerNet) Start() { |
||||||
|
//---- options ---------------------------------------
|
||||||
|
serverOpt := rup.Options.Get("server") |
||||||
|
maxSend := uint32(serverOpt.GetInt("maxSend", 40)) |
||||||
|
maxReceive := uint32(serverOpt.GetInt("maxReceive", 40)) |
||||||
|
addr := serverOpt.GetString("listenAddr", ":8081") |
||||||
|
acceptTimeout := time.Duration(serverOpt.GetInt("acceptTimeout", 2)) * time.Second |
||||||
|
writeTimeout := time.Duration(serverOpt.GetInt("writeTimeout", 5)) * time.Second |
||||||
|
|
||||||
|
//database options
|
||||||
|
gpsData := rup.Options.Get("gpsdata") |
||||||
|
storage := gpsData.GetString("storage", "sql") |
||||||
|
|
||||||
|
dbOpt := gpsData.Get(storage) |
||||||
|
dbDriver := dbOpt.GetString("driver", "postgres") |
||||||
|
dbInfo := dbOpt.GetString("connection", "") |
||||||
|
dbMaxIdle := dbOpt.GetInt("maxIdle", 0) |
||||||
|
dbMaxOpen := dbOpt.GetInt("maxOpen", 5) |
||||||
|
dbMaxLifetime := time.Duration(dbOpt.GetInt("maxLifetime", 60)) * time.Second |
||||||
|
|
||||||
|
//devicePrs options
|
||||||
|
rupOpt := parser.NewDevicePrsOptions(rup.Options) |
||||||
|
|
||||||
|
//server ID
|
||||||
|
serverId = serverOpt.GetString("id", "undefined (check configuration)") |
||||||
|
|
||||||
|
//listen to TCP
|
||||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", addr) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error resolving address %v\n", addr) |
||||||
|
return |
||||||
|
} |
||||||
|
listener, err := net.ListenTCP("tcp", tcpAddr) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error listening to tcp address %v\n", tcpAddr) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
//open database connection
|
||||||
|
db, err := sql.Open(dbDriver, dbInfo) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error opening db %v\n", dbDriver) |
||||||
|
return |
||||||
|
} |
||||||
|
defer db.Close() |
||||||
|
|
||||||
|
err = db.Ping() |
||||||
|
if err != nil { |
||||||
|
log.Printf("DB connection error: %v\n", err) |
||||||
|
return |
||||||
|
} |
||||||
|
db.SetMaxIdleConns(dbMaxIdle) |
||||||
|
db.SetMaxOpenConns(dbMaxOpen) |
||||||
|
db.SetConnMaxLifetime(dbMaxLifetime) |
||||||
|
|
||||||
|
//create procedure broker
|
||||||
|
broker := rupOpt.BrokerServer |
||||||
|
procdr, err := kafka.NewProducer(&kafka.ConfigMap{"bootstrap.servers": broker}) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to create producer: %s\n", err) |
||||||
|
os.Exit(1) |
||||||
|
} |
||||||
|
log.Printf("Created Producer %v\n", procdr) |
||||||
|
|
||||||
|
// creates a server
|
||||||
|
config := &gotcp.Config{ |
||||||
|
PacketSendChanLimit: maxSend, |
||||||
|
PacketReceiveChanLimit: maxReceive, |
||||||
|
} |
||||||
|
proto := &DevicePrsServerProtocol{ |
||||||
|
maxPacketSize: parser.NewDevicePrs().MaxPacketSize, |
||||||
|
} |
||||||
|
|
||||||
|
//map for storing various statistics
|
||||||
|
callback := &DevicePrsServerCallback{ |
||||||
|
options: rupOpt, |
||||||
|
gpsDb: db, |
||||||
|
writeTimeout: writeTimeout, |
||||||
|
procdr: procdr, |
||||||
|
} |
||||||
|
|
||||||
|
srv := gotcp.NewServer(config, callback, proto) |
||||||
|
rup.Server = srv |
||||||
|
rup.Callback = callback |
||||||
|
|
||||||
|
// starts service
|
||||||
|
go srv.Start(listener, acceptTimeout) |
||||||
|
log.Printf("listening: %v\n", listener.Addr()) |
||||||
|
|
||||||
|
// catchs system signal
|
||||||
|
chSig := make(chan os.Signal) |
||||||
|
signal.Notify(chSig, syscall.SIGINT, syscall.SIGTERM) |
||||||
|
ch := <-chSig |
||||||
|
log.Printf("Signal: %v\n", ch) |
||||||
|
|
||||||
|
// stops service
|
||||||
|
srv.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
//DevicePrsServerCallback holds server callback info
|
||||||
|
type DevicePrsServerCallback struct { |
||||||
|
options *parser.DevicePrsOptions |
||||||
|
gpsDb *sql.DB |
||||||
|
writeTimeout time.Duration |
||||||
|
procdr *kafka.Producer |
||||||
|
} |
||||||
|
|
||||||
|
//OnConnect is called when new connection established
|
||||||
|
func (cb *DevicePrsServerCallback) OnConnect(c *gotcp.Conn) bool { |
||||||
|
addr := c.GetRawConn().RemoteAddr() |
||||||
|
c.PutExtraData(addr) |
||||||
|
log.Printf("%v New connection(client: %v)\n", serverId,addr) |
||||||
|
|
||||||
|
//insert to array new address
|
||||||
|
var connDevice ConnectionDevice |
||||||
|
connDevice.imei = 0 |
||||||
|
connDevice.timeProcess = time.Now() |
||||||
|
connectionDevices.AddUpdt(addr.String(),connDevice) |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
//OnMessage is called when packet readed
|
||||||
|
func (cb *DevicePrsServerCallback) OnMessage(c *gotcp.Conn, p gotcp.Packet) bool { |
||||||
|
addr := c.GetRawConn().RemoteAddr() |
||||||
|
|
||||||
|
//downcast packet to devicePrs packet
|
||||||
|
packet := p.(*DevicePrsServerPacket) |
||||||
|
data := packet.Serialize() |
||||||
|
//fmt.Println(hex.EncodeToString(data))
|
||||||
|
|
||||||
|
//imei is empty so, we have to add last imei to the data
|
||||||
|
connectionDevice := connectionDevices.Value(addr.String()) |
||||||
|
|
||||||
|
//create parser
|
||||||
|
par := parser.NewDevicePrsParser(cb.options, cb.gpsDb, data, cb.procdr) |
||||||
|
if err := par.GetError(); err != nil { |
||||||
|
log.Printf("Error verifying packet(client: %v): %v, Err=%v\n", |
||||||
|
addr,hex.EncodeToString(data), err) |
||||||
|
} |
||||||
|
|
||||||
|
//set imei in array imei
|
||||||
|
imei_ := connectionDevice.imei |
||||||
|
if(imei_ == 0){ |
||||||
|
imei_ = par.IMEI |
||||||
|
connectionDevice.imei = par.IMEI |
||||||
|
connectionDevice.data = data |
||||||
|
connectionDevices.AddUpdt(addr.String(),connectionDevice) |
||||||
|
} |
||||||
|
|
||||||
|
duration := time.Since(connectionDevice.timeProcess) |
||||||
|
log.Printf("Data received(client: %v, IMEI: %v) with duration %v s\n", addr, imei_,math.Round(duration.Seconds()*100)/100) |
||||||
|
|
||||||
|
//save to database
|
||||||
|
statusInsert := par.ExecuteAsync() |
||||||
|
|
||||||
|
if(statusInsert){ |
||||||
|
// send response to client (ACK or NACK)
|
||||||
|
rep := par.GetClientResponse() |
||||||
|
c.AsyncWritePacket(NewDevicePrsServerPacket(rep), cb.writeTimeout) |
||||||
|
log.Printf("Sent ACK (client: %v, IMEI: %v)\n", addr, imei_) |
||||||
|
} |
||||||
|
|
||||||
|
//send message if record command
|
||||||
|
//if par.GetCommand() == model.CMD_RUP_RECORD {
|
||||||
|
// msg, err := par.GetBroadcastMessage()
|
||||||
|
// if len(msg) > 0 {
|
||||||
|
// c.AsyncWritePacket(NewRuptelaServerPacket(msg), cb.writeTimeout)
|
||||||
|
// } else if err != nil {
|
||||||
|
// log.Printf("Get message error(client: %v, IMEI: %v) = %v\n", addr, par.IMEI,err)
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//run parallel processing to handle packet parsing
|
||||||
|
//go par.ExecuteAsync()
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
//OnClose is called when connection being closed
|
||||||
|
func (cb *DevicePrsServerCallback) OnClose(c *gotcp.Conn) { |
||||||
|
addr := c.GetRawConn().RemoteAddr() |
||||||
|
connectionDevice := connectionDevices.Value(addr.String()) |
||||||
|
duration := time.Since(connectionDevice.timeProcess) |
||||||
|
|
||||||
|
connectionDevices.Del(addr.String()) |
||||||
|
log.Printf("%v Close connection (client: %v, IMEI: %v) with duration %v s\n", serverId ,c.GetExtraData(),connectionDevice.imei,math.Round(duration.Seconds()*100)/100) |
||||||
|
} |
||||||
|
|
||||||
|
//-------------------------------------------------------------------
|
||||||
|
|
||||||
|
//DevicePrsServerProtocol holds parser
|
||||||
|
type DevicePrsServerProtocol struct { |
||||||
|
maxPacketSize int |
||||||
|
} |
||||||
|
|
||||||
|
//ReadPacket called everytime data is available
|
||||||
|
func (p *DevicePrsServerProtocol) ReadPacket(conn *net.TCPConn) (gotcp.Packet, error) { |
||||||
|
var ( |
||||||
|
lengthBytes = make([]byte, 2) |
||||||
|
length uint16 |
||||||
|
) |
||||||
|
|
||||||
|
//TODO handle packet other than Device Record
|
||||||
|
//OK
|
||||||
|
|
||||||
|
// read length
|
||||||
|
if _, err := io.ReadFull(conn, lengthBytes); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
Limit := uint16(p.maxPacketSize) |
||||||
|
if length = binary.BigEndian.Uint16(lengthBytes); length > Limit { |
||||||
|
return nil, errors.New("The size of packet is larger than the limit") |
||||||
|
} |
||||||
|
|
||||||
|
log.Printf("data packet length(client: %v) : %d", conn.RemoteAddr(), length) |
||||||
|
|
||||||
|
//Packet structure
|
||||||
|
//LEN + DATA + CRC16
|
||||||
|
buff := make([]byte, 2+length+2) |
||||||
|
copy(buff[0:2], lengthBytes) |
||||||
|
|
||||||
|
// read body ( buff = lengthBytes + body )
|
||||||
|
if _, err := io.ReadFull(conn, buff[2:]); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return NewDevicePrsServerPacket(buff), nil |
||||||
|
} |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,60 @@ |
|||||||
|
{ |
||||||
|
server: { |
||||||
|
id: teltonika9000 |
||||||
|
listenAddr: ":9000" |
||||||
|
acceptTimeout: 10 # timeout (dalam detik) |
||||||
|
writeTimeout: 10 # timeout (dalam detik) |
||||||
|
maxReceive: 50 |
||||||
|
maxSend: 50 |
||||||
|
device: ruptela |
||||||
|
} |
||||||
|
|
||||||
|
ruptela: { |
||||||
|
message: "http://192.168.70.200/oslog/api/BroadcastPrivate/getbroadcastmessage/" |
||||||
|
report: "http://192.168.70.200/oslog/api/BroadcastPrivate/putbroadcastmessage" |
||||||
|
appendIMEI: false |
||||||
|
serviceTimeout: 10 # REST Service timeout (dalam detik) |
||||||
|
} |
||||||
|
|
||||||
|
gpsdata: { |
||||||
|
storage: sql |
||||||
|
sql: { |
||||||
|
driver: postgres |
||||||
|
connection: "user=postgres password=s3mb1l4n dbname=OSLOGRECV2 host=192.168.70.196 port=5432 connect_timeout=30 sslmode=disable" |
||||||
|
maxIdle: 10 #Max jumlah koneksi idle |
||||||
|
maxOpen: 10 #Max jumlah koneksi open |
||||||
|
maxLifetime: 60 #Maximum lifetime (dalam detik) |
||||||
|
insertQuery: INSERT INTO "GPS_DATA"("IMEI", "DATA_LOG", "FLAG", "DATE_INS", "DESC", "GPS_CODE", "DATA_LEN") VALUES($1, $2, $3, $4, $5, $6, $7) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
log: { |
||||||
|
#Known types: console, file, sql |
||||||
|
type: console, file |
||||||
|
console: { |
||||||
|
# use default options |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#for file (uncomment type) |
||||||
|
# type: file |
||||||
|
file: { |
||||||
|
name: /home/vagrant/receiver/teltonika9000/applog.log |
||||||
|
append: true |
||||||
|
} |
||||||
|
|
||||||
|
#for sql (uncomment type) |
||||||
|
# SQLite -> driver: sqlite3 |
||||||
|
# PostgreSQL -> driver: postgres |
||||||
|
# type: sql |
||||||
|
sql: { |
||||||
|
driver: sqlite3 |
||||||
|
connection: ./applog.sqlite |
||||||
|
maxIdle: 10 #Max jumlah koneksi idle |
||||||
|
maxOpen: 10 #Max jumlah koneksi open |
||||||
|
maxLifetime: 60 #Maximum lifetime (dalam detik) |
||||||
|
createQuery: CREATE TABLE applog(id INTEGER PRIMARY KEY AUTOINCREMENT, ts DATETIME, app VARCHAR(100), content TEXT) |
||||||
|
insertQuery: INSERT INTO applog(ts, app, content) VALUES(?, ?, ?) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,160 @@ |
|||||||
|
{ |
||||||
|
server: { |
||||||
|
id: ruptela |
||||||
|
listenAddr: ":4000" |
||||||
|
acceptTimeout: 10 # timeout (dalam detik) |
||||||
|
writeTimeout: 10 # timeout (dalam detik) |
||||||
|
maxReceive: 50 |
||||||
|
maxSend: 50 |
||||||
|
device: RUPTELA |
||||||
|
} |
||||||
|
|
||||||
|
ruptela: { |
||||||
|
message: "http://localhost:8081/api/" |
||||||
|
report: "http://localhost:8081/reportAPI" |
||||||
|
appendIMEI: false |
||||||
|
serviceTimeout: 10 # REST Service timeout (dalam detik) |
||||||
|
} |
||||||
|
|
||||||
|
gpsdata: { |
||||||
|
storage: sql |
||||||
|
sql: { |
||||||
|
driver: postgres |
||||||
|
connection: "user=postgres password=s3mb1l4n dbname=OSLOGREC host=127.0.0.1 port=5432 connect_timeout=30 sslmode=disable" |
||||||
|
maxIdle: 10 #Max jumlah koneksi idle |
||||||
|
maxOpen: 10 #Max jumlah koneksi open |
||||||
|
maxLifetime: 60 #Maximum lifetime (dalam detik) |
||||||
|
insertQuery: INSERT INTO "GPS_DATA"("IMEI", "DATA_LOG", "FLAG", "DATE_INS", "DESC", "GPS_CODE", "DATA_LEN") VALUES($1, $2, $3, $4, $5, $6, $7) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
log: { |
||||||
|
#Known types: console, file, sql |
||||||
|
type: console, file |
||||||
|
console: { |
||||||
|
# use default options |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#for file (uncomment type) |
||||||
|
# type: file |
||||||
|
file: { |
||||||
|
name: /Users/baymac/Documents/work/IU/oslog/sourcecode/oslog.id/putu/jyoti_teltonika/server/exe/applog.log |
||||||
|
append: true |
||||||
|
} |
||||||
|
|
||||||
|
#for sql (uncomment type) |
||||||
|
# SQLite -> driver: sqlite3 |
||||||
|
# PostgreSQL -> driver: postgres |
||||||
|
# type: sql |
||||||
|
sql: { |
||||||
|
driver: sqlite3 |
||||||
|
connection: ./applog.sqlite |
||||||
|
maxIdle: 10 #Max jumlah koneksi idle |
||||||
|
maxOpen: 10 #Max jumlah koneksi open |
||||||
|
maxLifetime: 60 #Maximum lifetime (dalam detik) |
||||||
|
createQuery: CREATE TABLE applog(id INTEGER PRIMARY KEY AUTOINCREMENT, ts DATETIME, app VARCHAR(100), content TEXT) |
||||||
|
insertQuery: INSERT INTO applog(ts, app, content) VALUES(?, ?, ?) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
messagebroker: { |
||||||
|
brokerServer: localhost:9092 |
||||||
|
brokerTopic: ruptela4000 |
||||||
|
} |
||||||
|
|
||||||
|
iotag: { |
||||||
|
tags: [ |
||||||
|
{ |
||||||
|
tagName: Din1 |
||||||
|
tagId: 2 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Din2 |
||||||
|
tagId: 3 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Din3 |
||||||
|
tagId: 4 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Din4 |
||||||
|
tagId: 5 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Ain1 |
||||||
|
tagId: 22 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Ain2 |
||||||
|
tagId: 23 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Dist |
||||||
|
tagId: 77 |
||||||
|
tagDataType: float32 |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: DistTotal |
||||||
|
tagId: 65 |
||||||
|
tagDataType: float64 |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: GsmSignalSensor |
||||||
|
tagId: 27 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: DeepSleepSensor |
||||||
|
tagId: 200 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: HarshBreaking |
||||||
|
tagId: 135 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: HarshAcceleration |
||||||
|
tagId: 136 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: BatteryVolt |
||||||
|
tagId: 30 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: PowerVolt |
||||||
|
tagId: 29 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Temp |
||||||
|
tagId: 78 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Temp1 |
||||||
|
tagId: 79 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Temp2 |
||||||
|
tagId: 80 |
||||||
|
tagDataType: int |
||||||
|
}, |
||||||
|
{ |
||||||
|
tagName: Rfid |
||||||
|
tagId: 171 |
||||||
|
tagDataType: string |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,102 @@ |
|||||||
|
//
|
||||||
|
// Device server which handle communication to various devices (currently implements RUPTELA)
|
||||||
|
//
|
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"log" |
||||||
|
"runtime" |
||||||
|
"os" |
||||||
|
"io/ioutil" |
||||||
|
"strconv" |
||||||
|
"path/filepath" |
||||||
|
|
||||||
|
"github.com/ipsusila/opt" |
||||||
|
//"../../model"
|
||||||
|
"../../server" |
||||||
|
"../../lumberjack" |
||||||
|
|
||||||
|
_ "github.com/lib/pq" |
||||||
|
_ "github.com/mattn/go-sqlite3" |
||||||
|
) |
||||||
|
|
||||||
|
var exPath = ""; |
||||||
|
|
||||||
|
func startServer(options *opt.Options) { |
||||||
|
//---options ---------------------------------------------------------
|
||||||
|
//start server according to mode
|
||||||
|
svcNet := &server.DevicePrsServerNet{ |
||||||
|
Options: options, |
||||||
|
} |
||||||
|
svcNet.Start() |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
//maximum cpu
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU()) |
||||||
|
|
||||||
|
ex, err := os.Executable() |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
exPath = filepath.Dir(ex) + "/" |
||||||
|
confPath := exPath + "config.hjson" |
||||||
|
// re-open file
|
||||||
|
file, err := os.Open(confPath) |
||||||
|
if err != nil { |
||||||
|
confPath = "config.hjson" |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
|
||||||
|
//parse configurationf file
|
||||||
|
cfgFile := flag.String("conf", confPath, "Configuration file") |
||||||
|
flag.Parse() |
||||||
|
|
||||||
|
//load options
|
||||||
|
config, err := opt.FromFile(*cfgFile, opt.FormatAuto) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while loading configuration file %v -> %v\n", *cfgFile, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
//display server ID (displayed in console)
|
||||||
|
log.Printf("Server ID: %v\n", config.Get("server").GetString("id", "undefined (check configuration)")) |
||||||
|
|
||||||
|
//setup log
|
||||||
|
//lw, err := utility.NewLogWriter(config)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Printf("Error while setup logging: %v\n", err)
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime) |
||||||
|
//log.SetOutput(lw)
|
||||||
|
logName := config.Get("log").Get("file").GetString("name", "") |
||||||
|
log.SetOutput(&lumberjack.Logger{ |
||||||
|
Filename: logName, |
||||||
|
MaxSize: 10, // megabytes
|
||||||
|
MaxBackups: 3, |
||||||
|
MaxAge: 3, //days
|
||||||
|
Compress: true, // disabled by default
|
||||||
|
}) |
||||||
|
//defer lw.Close()
|
||||||
|
|
||||||
|
pidFilePath := exPath + "receiver-" + config.Get("server").GetString("id", "undefined (check configuration)") + "-pid" |
||||||
|
errRemoveFile := os.Remove(pidFilePath) |
||||||
|
if errRemoveFile != nil { |
||||||
|
log.Printf("PID file will be created") |
||||||
|
} |
||||||
|
pidStr := strconv.Itoa(os.Getpid()) |
||||||
|
log.Printf("PID: %v\n", pidStr) |
||||||
|
d1 := []byte(pidStr) |
||||||
|
errCreateFile := ioutil.WriteFile(pidFilePath, d1, 0644) |
||||||
|
if errCreateFile != nil { |
||||||
|
log.Printf("Error while setup write PID file: %v\n", errCreateFile) |
||||||
|
} else{ |
||||||
|
log.Printf("PID file created") |
||||||
|
} |
||||||
|
|
||||||
|
//Run application
|
||||||
|
startServer(config) |
||||||
|
} |
@ -0,0 +1,133 @@ |
|||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/hex" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"../model" |
||||||
|
) |
||||||
|
|
||||||
|
/* |
||||||
|
Send data to server |
||||||
|
*/ |
||||||
|
func sendData(conf *model.ServerConfig, data []byte, dur chan<- int64) error { |
||||||
|
//simulate client
|
||||||
|
stTime := time.Now() |
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", conf.IP, conf.Port) |
||||||
|
conn, err := net.Dial("tcp", addr) |
||||||
|
if err != nil { |
||||||
|
fmt.Println("Error: ", err) |
||||||
|
dur <- -1 |
||||||
|
return err |
||||||
|
} |
||||||
|
defer conn.Close() |
||||||
|
|
||||||
|
nw, err := conn.Write(data) |
||||||
|
if err != nil { |
||||||
|
fmt.Printf("Write error: %+v\n", err) |
||||||
|
dur <- -1 |
||||||
|
return err |
||||||
|
} |
||||||
|
fmt.Println("Packet writen = ", nw) |
||||||
|
//sleep awhile
|
||||||
|
//time.Sleep(time.Second)
|
||||||
|
|
||||||
|
//read response
|
||||||
|
rep := make([]byte, 20) |
||||||
|
nr, err := conn.Read(rep) |
||||||
|
if err != nil { |
||||||
|
fmt.Printf("Read error: %+v\n", err) |
||||||
|
dur <- -1 |
||||||
|
return err |
||||||
|
} |
||||||
|
fmt.Println("Response -> ", rep[:nr]) |
||||||
|
dur <- time.Now().Sub(stTime).Nanoseconds() |
||||||
|
return nil |
||||||
|
} |
||||||
|
func TestInvalid(t *testing.T) { |
||||||
|
//config
|
||||||
|
conf := model.ServerConfig{IP: "127.0.0.1", Port: 8095} |
||||||
|
|
||||||
|
//test failed data
|
||||||
|
dur := make(chan int64, 1) |
||||||
|
data3 := []byte{0x08, 0x00, 0x01, 0x02} |
||||||
|
sendData(&conf, data3, dur) |
||||||
|
<-dur |
||||||
|
} |
||||||
|
|
||||||
|
//start sending data
|
||||||
|
func TestRuptelaReceiver(t *testing.T) { |
||||||
|
|
||||||
|
//send data
|
||||||
|
data1, _ := hex.DecodeString( |
||||||
|
"007900000b1a2a5585c30100024e9c036900000f101733208ff45e07b31b570a001009090605011b1a020003001c01ad01021d338e1600000" + |
||||||
|
"2960000601a41014bc16d004e9c038400000f104fdf20900d20075103b00a001308090605011b1a020003001c01ad01021d33b11600000296" + |
||||||
|
"0000601a41014bc1ea0028f9") |
||||||
|
|
||||||
|
data2, _ := hex.DecodeString( |
||||||
|
"033500000C076B5C208F01011E5268CEF20000196E3A3A0AEF3E934F3E2D780000000007000000005268CEFD0000196E3A3A0AEF3E934F3" + |
||||||
|
"E2D780000000007000000005268CF080000196E3A3A0AEF3E934F3E2D780000000007000000005268CF130000196E3A3A0AEF3E934F3E2D7" + |
||||||
|
"80000000007000000005268CF1E0000196E3A3A0AEF3E934F3E2D780000000007000000005268CF290000196E3A3A0AEF3E934F3E2D78000" + |
||||||
|
"0000007000000005268CF340000196E3A3A0AEF3E934F3E2D780000000007000000005268CF3F0000196E3A3A0AEF3E934F3E2D780000000" + |
||||||
|
"007000000005268CF4A0000196E3A3A0AEF3E934F3E2D780000000007000000005268CF550000196E3A3A0AEF3E934F3E2D7800000000070" + |
||||||
|
"00000005268CF600000196E3A3A0AEF3E934F3E2D780000000007000000005268CF6B0000196E3A3A0AEF3E934F3E2D78000000000700000" + |
||||||
|
"0005268CF730000196E36630AEF42CE4F6D0BF40400022208000000005268CF7E0000196E36B60AEF42BE4F6D0BF400000000070000000052" + |
||||||
|
"68CF890000196E36B60AEF42BE4F6D0BF40000000007000000005268CF940000196E36B60AEF42BE4F6D0BF40000000007000000005268CF" + |
||||||
|
"9F0000196E36B60AEF42BE4F6D0BF40000000007000000005268CFAA0000196E36B60AEF42BE4F6D0BF40000000007000000005268CFB50" + |
||||||
|
"000196E36B60AEF42BE4F6D0BF40000000007000000005268CFC00000196E36B60AEF42BE4F6D0BF40000000007000000005268CFCB00001" + |
||||||
|
"96E36B60AEF42BE4F6D0BF40000000007000000005268CFD60000196E36B60AEF42BE4F6D0BF40000000007000000005268CFD70000196E" + |
||||||
|
"3C710AEF5EFF4F690BF40400011708000000005268CFE20000196E3B980AEF601A4F690BF40000000007000000005268CFED0000196E3B980" + |
||||||
|
"AEF601A4F690BF40000000007000000005268CFF80000196E3B980AEF601A4F690BF40000000007000000005268D0030000196E3B980AEF60" + |
||||||
|
"1A4F690BF40000000007000000005268D00E0000196E3B980AEF601A4F690BF40000000007000000005268D0190000196E3B980AEF601A4F6" + |
||||||
|
"90BF40000000007000000005268D0240000196E3B980AEF601A4F690BF400000000070000000046E2") |
||||||
|
|
||||||
|
data3, _ := hex.DecodeString( |
||||||
|
"0011000315A07F44865A0E01000053EA01DF65AD6D") |
||||||
|
|
||||||
|
const NTEST = 1 |
||||||
|
conf := model.ServerConfig{IP: "127.0.0.1", Port: 8081} |
||||||
|
ch := make(chan int64, NTEST*3) |
||||||
|
|
||||||
|
n := 0 |
||||||
|
for i := 0; i < NTEST; i++ { |
||||||
|
go sendData(&conf, data1, ch) |
||||||
|
n++ |
||||||
|
go sendData(&conf, data2, ch) |
||||||
|
n++ |
||||||
|
|
||||||
|
go sendData(&conf, data3, ch) |
||||||
|
n++ |
||||||
|
} |
||||||
|
|
||||||
|
var ma int64 = 0 |
||||||
|
var mi int64 = 10000000000000 |
||||||
|
var tot float64 = 0 |
||||||
|
|
||||||
|
var ti int64 |
||||||
|
for i := 0; i < NTEST*2; i++ { |
||||||
|
ti = <-ch |
||||||
|
if ti < 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if ti < mi { |
||||||
|
mi = ti |
||||||
|
} |
||||||
|
|
||||||
|
if ti > ma { |
||||||
|
ma = ti |
||||||
|
} |
||||||
|
|
||||||
|
tot += float64(ti) |
||||||
|
} |
||||||
|
|
||||||
|
const MILIS = 1000000 |
||||||
|
avg := tot / (NTEST * 2) |
||||||
|
fmt.Printf("Total time = %+v ms, avg = %+v ms, mi = %+v ms, ma = %+v ms\n", |
||||||
|
tot/MILIS, avg/MILIS, float64(mi)/MILIS, float64(ma)/MILIS) |
||||||
|
|
||||||
|
} |
@ -0,0 +1,172 @@ |
|||||||
|
package utility |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"time" |
||||||
|
|
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/ipsusila/opt" |
||||||
|
) |
||||||
|
|
||||||
|
//SQLWriter write log to database i.e. SQLite
|
||||||
|
type SQLWriter struct { |
||||||
|
db *sql.DB |
||||||
|
insertQuery string |
||||||
|
createQuery string |
||||||
|
appID string |
||||||
|
} |
||||||
|
|
||||||
|
//LogWriter support multiple output
|
||||||
|
type LogWriter struct { |
||||||
|
writers []io.WriteCloser |
||||||
|
console bool |
||||||
|
} |
||||||
|
|
||||||
|
//NewLogWriter create multiple writer
|
||||||
|
func NewLogWriter(options *opt.Options) (*LogWriter, error) { |
||||||
|
logOpt := options.Get("log") |
||||||
|
|
||||||
|
//backward compatibility, get from server
|
||||||
|
appID := options.Get("server").GetString("id", "") |
||||||
|
if appID == "" { |
||||||
|
//if failed, get from `log.id`
|
||||||
|
appID = logOpt.GetString("id", "") |
||||||
|
} |
||||||
|
if appID == "" { |
||||||
|
//if failed, get from `id``
|
||||||
|
appID = options.GetString("id", "*undefined*") |
||||||
|
} |
||||||
|
|
||||||
|
outTypes := logOpt.GetString("type", "") |
||||||
|
typeItems := strings.Split(outTypes, ",") |
||||||
|
writers := []io.WriteCloser{} |
||||||
|
|
||||||
|
console := false |
||||||
|
for _, item := range typeItems { |
||||||
|
item = strings.TrimSpace(item) |
||||||
|
switch item { |
||||||
|
case "console": |
||||||
|
console = true |
||||||
|
case "file": |
||||||
|
fileOpt := logOpt.Get("file") |
||||||
|
name := fileOpt.GetString("name", "") |
||||||
|
appendExisting := fileOpt.GetBool("append", true) |
||||||
|
flag := os.O_WRONLY | os.O_CREATE |
||||||
|
if appendExisting { |
||||||
|
flag |= os.O_APPEND |
||||||
|
} else { |
||||||
|
flag |= os.O_TRUNC |
||||||
|
} |
||||||
|
|
||||||
|
f, err := os.OpenFile(name, flag, 0666) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
writers = append(writers, f) |
||||||
|
case "sql": |
||||||
|
w, err := newSQLWriter(logOpt, appID) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
writers = append(writers, w) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
lw := &LogWriter{ |
||||||
|
writers: writers, |
||||||
|
console: console, |
||||||
|
} |
||||||
|
|
||||||
|
return lw, nil |
||||||
|
} |
||||||
|
|
||||||
|
//NewSQLWriter open log writer to sql
|
||||||
|
func newSQLWriter(logOpt *opt.Options, appID string) (*SQLWriter, error) { |
||||||
|
sqlOpt := logOpt.Get("sql") |
||||||
|
dbDriver := sqlOpt.GetString("driver", "") |
||||||
|
dbInfo := sqlOpt.GetString("connection", "") |
||||||
|
createQuery := sqlOpt.GetString("createQuery", "") |
||||||
|
insertQuery := sqlOpt.GetString("insertQuery", "") |
||||||
|
dbMaxIdle := sqlOpt.GetInt("maxIdle", 0) |
||||||
|
dbMaxOpen := sqlOpt.GetInt("maxOpen", 5) |
||||||
|
dbMaxLifetime := time.Duration(sqlOpt.GetInt("maxLifetime", 60)) * time.Second |
||||||
|
|
||||||
|
//open databae connection
|
||||||
|
db, err := sql.Open(dbDriver, dbInfo) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
err = db.Ping() |
||||||
|
if err != nil { |
||||||
|
db.Close() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
//connection pooling
|
||||||
|
db.SetMaxIdleConns(dbMaxIdle) |
||||||
|
db.SetMaxOpenConns(dbMaxOpen) |
||||||
|
db.SetConnMaxLifetime(dbMaxLifetime) |
||||||
|
|
||||||
|
//ignore error
|
||||||
|
//TODO, verify query (security, untrusted source)
|
||||||
|
db.Exec(createQuery) |
||||||
|
|
||||||
|
w := &SQLWriter{ |
||||||
|
db: db, |
||||||
|
createQuery: createQuery, |
||||||
|
insertQuery: insertQuery, |
||||||
|
appID: appID, |
||||||
|
} |
||||||
|
|
||||||
|
return w, nil |
||||||
|
} |
||||||
|
|
||||||
|
//Write to multiple log
|
||||||
|
func (lw *LogWriter) Write(data []byte) (n int, err error) { |
||||||
|
//now := time.Now().Format("[2006-01-02 15:04:05]")
|
||||||
|
if lw.console { |
||||||
|
n, err = os.Stderr.Write(data) |
||||||
|
} |
||||||
|
for _, w := range lw.writers { |
||||||
|
n, err = w.Write(data) |
||||||
|
} |
||||||
|
|
||||||
|
return n, err |
||||||
|
} |
||||||
|
|
||||||
|
//Close all writer
|
||||||
|
func (lw *LogWriter) Close() error { |
||||||
|
for _, w := range lw.writers { |
||||||
|
w.Close() |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
//Write data with timestamp (ts, log)
|
||||||
|
func (w *SQLWriter) Write(data []byte) (int, error) { |
||||||
|
//TODO, verify query (security, untrusted source)
|
||||||
|
result, err := w.db.Exec(w.insertQuery, time.Now(), w.appID, string(data)) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
n := len(data) |
||||||
|
_, err = result.LastInsertId() |
||||||
|
if err != nil { |
||||||
|
return n, err |
||||||
|
} |
||||||
|
|
||||||
|
nrows, err := result.RowsAffected() |
||||||
|
if err != nil || nrows == 0 { |
||||||
|
return n, err |
||||||
|
} |
||||||
|
|
||||||
|
return n, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (w *SQLWriter) Close() error { |
||||||
|
return w.db.Close() |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
{ |
||||||
|
watch: { |
||||||
|
paths: ["../../", "doesnotexist"] |
||||||
|
maxFile: 5 |
||||||
|
pattern: server |
||||||
|
verbose: true |
||||||
|
catchSignal: false |
||||||
|
} |
||||||
|
|
||||||
|
log: { |
||||||
|
#Known types: console, file, sql |
||||||
|
type: console, sql |
||||||
|
console: { |
||||||
|
# use default options |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
#for file (uncomment type) |
||||||
|
# type: file |
||||||
|
file: { |
||||||
|
name: ./applog.log |
||||||
|
append: true |
||||||
|
} |
||||||
|
|
||||||
|
#for sql (uncomment type) |
||||||
|
# SQLite -> driver: sqlite3 |
||||||
|
# PostgreSQL -> driver: postgres |
||||||
|
# type: sql |
||||||
|
sql: { |
||||||
|
driver: sqlite3 |
||||||
|
connection: ./applog.sqlite |
||||||
|
maxIdle: 10 #Max jumlah koneksi idle |
||||||
|
maxOpen: 10 #Max jumlah koneksi open |
||||||
|
maxLifetime: 60 #Maximum lifetime (dalam detik) |
||||||
|
createQuery: CREATE TABLE applog(id INTEGER PRIMARY KEY AUTOINCREMENT, ts DATETIME, app VARCHAR(100), content TEXT) |
||||||
|
insertQuery: INSERT INTO applog(ts, app, content) VALUES(?, ?, ?) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,340 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
"path/filepath" |
||||||
|
"regexp" |
||||||
|
"sync" |
||||||
|
"syscall" |
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify" |
||||||
|
"github.com/ipsusila/opt" |
||||||
|
"oslog.id/putu/jyoti/utility" |
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3" |
||||||
|
) |
||||||
|
|
||||||
|
type watcherOptions struct { |
||||||
|
watchPaths []string |
||||||
|
maxLastFile int |
||||||
|
pattern string |
||||||
|
verbose bool |
||||||
|
catchSignal bool |
||||||
|
regPattern *regexp.Regexp |
||||||
|
} |
||||||
|
|
||||||
|
type node struct { |
||||||
|
name string |
||||||
|
fullName string |
||||||
|
next *node |
||||||
|
} |
||||||
|
|
||||||
|
type list struct { |
||||||
|
sync.Mutex |
||||||
|
numItems int |
||||||
|
head *node |
||||||
|
tail *node |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) clear() { |
||||||
|
l.head = nil |
||||||
|
l.tail = nil |
||||||
|
l.numItems = 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) count() int { |
||||||
|
l.Lock() |
||||||
|
defer l.Unlock() |
||||||
|
|
||||||
|
return l.numItems |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) newNode(name string) (*node, error) { |
||||||
|
fullName, err := filepath.Abs(name) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
nd := &node{ |
||||||
|
name: name, |
||||||
|
fullName: fullName, |
||||||
|
} |
||||||
|
|
||||||
|
return nd, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) push(name string) error { |
||||||
|
l.Lock() |
||||||
|
defer l.Unlock() |
||||||
|
|
||||||
|
nd, err := l.newNode(name) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if l.tail == nil { |
||||||
|
l.head = nd |
||||||
|
l.tail = nd |
||||||
|
} else { |
||||||
|
l.tail.next = nd |
||||||
|
l.tail = nd |
||||||
|
} |
||||||
|
|
||||||
|
l.numItems++ |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) peek() *node { |
||||||
|
l.Lock() |
||||||
|
defer l.Unlock() |
||||||
|
|
||||||
|
if l.head == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return l.head |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) safePop() *node { |
||||||
|
if l.head == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
nd := l.head |
||||||
|
l.head = nd.next |
||||||
|
if l.head == nil { |
||||||
|
l.tail = nil |
||||||
|
} |
||||||
|
l.numItems-- |
||||||
|
return nd |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) pop() *node { |
||||||
|
l.Lock() |
||||||
|
defer l.Unlock() |
||||||
|
|
||||||
|
return l.safePop() |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) remove(name string) *node { |
||||||
|
l.Lock() |
||||||
|
defer l.Unlock() |
||||||
|
|
||||||
|
//create item
|
||||||
|
item, err := l.newNode(name) |
||||||
|
if err != nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
//empty
|
||||||
|
if l.head == nil || item == nil { |
||||||
|
return nil |
||||||
|
} else if l.head.fullName == item.fullName { |
||||||
|
return l.safePop() |
||||||
|
} |
||||||
|
|
||||||
|
//node current and next
|
||||||
|
nd := l.head |
||||||
|
next := l.head.next |
||||||
|
for next != nil { |
||||||
|
if next.fullName == item.fullName { |
||||||
|
nextNext := next.next |
||||||
|
nd.next = nextNext |
||||||
|
l.numItems-- |
||||||
|
if next == l.tail { |
||||||
|
l.tail = nd |
||||||
|
} |
||||||
|
return next |
||||||
|
} |
||||||
|
|
||||||
|
//next items
|
||||||
|
nd = next |
||||||
|
next = nd.next |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (l *list) iterate(fn func(nd *node)) { |
||||||
|
l.Lock() |
||||||
|
defer l.Unlock() |
||||||
|
|
||||||
|
nd := l.head |
||||||
|
for nd != nil { |
||||||
|
fn(nd) |
||||||
|
nd = nd.next |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func startWatching(options *watcherOptions) { |
||||||
|
//file items
|
||||||
|
items := &list{} |
||||||
|
errItems := &list{} |
||||||
|
|
||||||
|
watcher, err := fsnotify.NewWatcher() |
||||||
|
if err != nil { |
||||||
|
log.Fatal(err) |
||||||
|
} |
||||||
|
|
||||||
|
done := make(chan bool, 1) |
||||||
|
mustExit := make(chan bool, 1) |
||||||
|
|
||||||
|
// Process events
|
||||||
|
go func() { |
||||||
|
defer func() { |
||||||
|
close(done) |
||||||
|
}() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case ev := <-watcher.Events: |
||||||
|
isCreate := ev.Op&fsnotify.Create == fsnotify.Create |
||||||
|
isDelete := ev.Op&fsnotify.Remove == fsnotify.Remove |
||||||
|
if isCreate { |
||||||
|
//match pattern
|
||||||
|
//when new items created, delete old items
|
||||||
|
//if number of items exceed threshold
|
||||||
|
if options.regPattern == nil || options.regPattern.MatchString(ev.Name) { |
||||||
|
items.push(ev.Name) |
||||||
|
if options.verbose { |
||||||
|
log.Printf("Item %v ADDED", ev.Name) |
||||||
|
} |
||||||
|
|
||||||
|
if items.count() > options.maxLastFile { |
||||||
|
node := items.pop() |
||||||
|
err := os.Remove(node.fullName) |
||||||
|
if err != nil { |
||||||
|
errItems.push(node.name) |
||||||
|
log.Printf("Error %v while deleting %v", err, node.fullName) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} else if isDelete { |
||||||
|
if options.verbose { |
||||||
|
log.Printf("Item %v REMOVED", ev.Name) |
||||||
|
} |
||||||
|
//if manually deleted: remove from list
|
||||||
|
items.remove(ev.Name) |
||||||
|
} |
||||||
|
case err := <-watcher.Errors: |
||||||
|
log.Println("Watcher error:", err) |
||||||
|
case <-mustExit: |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
//Start watching items
|
||||||
|
nwatched := 0 |
||||||
|
for _, sPath := range options.watchPaths { |
||||||
|
err = watcher.Add(sPath) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error watching `%s` -> %v", sPath, err) |
||||||
|
} else { |
||||||
|
nwatched++ |
||||||
|
if options.verbose { |
||||||
|
log.Printf("Watching %s...", sPath) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if nwatched == 0 { |
||||||
|
close(mustExit) |
||||||
|
} else { |
||||||
|
//catch signal
|
||||||
|
if options.catchSignal { |
||||||
|
signalChan := make(chan os.Signal, 1) |
||||||
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) |
||||||
|
go func() { |
||||||
|
for _ = range signalChan { |
||||||
|
fmt.Println("Received an interrupt, stopping watcher...") |
||||||
|
close(mustExit) |
||||||
|
return |
||||||
|
} |
||||||
|
}() |
||||||
|
} else { |
||||||
|
//wait for q/q
|
||||||
|
buf := make([]byte, 1) |
||||||
|
for { |
||||||
|
//wait input
|
||||||
|
n, err := os.Stdin.Read(buf) |
||||||
|
if err != nil { |
||||||
|
log.Println(err) |
||||||
|
} else if n > 0 && buf[0] == 'q' || buf[0] == 'Q' { |
||||||
|
close(mustExit) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
// Hang so program doesn't exit
|
||||||
|
<-done |
||||||
|
watcher.Close() |
||||||
|
|
||||||
|
//erritems
|
||||||
|
if errItems.count() > 0 { |
||||||
|
log.Printf("The following item(s) were not removed do to error") |
||||||
|
errItems.iterate(func(nd *node) { |
||||||
|
log.Printf("%v", nd.fullName) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
log.Printf("Watcher %d DONE", items.count()) |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
//parse configurationf file
|
||||||
|
cfgFile := flag.String("conf", "config.hjson", "Configuration file") |
||||||
|
flag.Parse() |
||||||
|
|
||||||
|
//load options
|
||||||
|
config, err := opt.FromFile(*cfgFile, opt.FormatAuto) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while loading configuration file %v -> %v\n", *cfgFile, err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
//display server ID (displayed in console)
|
||||||
|
log.Printf("Application ID: %v\n", config.GetString("id", "undefined (check configuration)")) |
||||||
|
|
||||||
|
//configure log writer
|
||||||
|
lw, err := utility.NewLogWriter(config) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while setup logging: %v\n", err) |
||||||
|
return |
||||||
|
} |
||||||
|
log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime) |
||||||
|
log.SetOutput(lw) |
||||||
|
|
||||||
|
defer lw.Close() |
||||||
|
|
||||||
|
//options
|
||||||
|
/* |
||||||
|
watchPaths []string |
||||||
|
maxLastFile int |
||||||
|
pattern string |
||||||
|
verbose bool |
||||||
|
catchSignal bool |
||||||
|
*/ |
||||||
|
options := &watcherOptions{} |
||||||
|
wcfg := config.Get("watch") |
||||||
|
options.watchPaths = wcfg.GetStringArray("paths") |
||||||
|
options.maxLastFile = wcfg.GetInt("maxFile", 50) |
||||||
|
options.pattern = wcfg.GetString("pattern", "") |
||||||
|
options.verbose = wcfg.GetBool("verbose", false) |
||||||
|
options.catchSignal = wcfg.GetBool("catchSignal", false) |
||||||
|
|
||||||
|
//verbose?
|
||||||
|
if options.verbose { |
||||||
|
log.Printf("%s", config.AsJSON()) |
||||||
|
} |
||||||
|
|
||||||
|
//compile regex
|
||||||
|
if len(options.pattern) > 0 { |
||||||
|
options.regPattern = regexp.MustCompile(options.pattern) |
||||||
|
} |
||||||
|
|
||||||
|
startWatching(options) |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2014 Jie Li |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||||
|
this software and associated documentation files (the "Software"), to deal in |
||||||
|
the Software without restriction, including without limitation the rights to |
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so, |
||||||
|
subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,25 @@ |
|||||||
|
gotcp |
||||||
|
================ |
||||||
|
|
||||||
|
A Go package for quickly building tcp servers |
||||||
|
|
||||||
|
|
||||||
|
Usage |
||||||
|
================ |
||||||
|
|
||||||
|
###Install |
||||||
|
|
||||||
|
~~~ |
||||||
|
go get github.com/gansidui/gotcp |
||||||
|
~~~ |
||||||
|
|
||||||
|
|
||||||
|
###Examples |
||||||
|
|
||||||
|
* [echo](https://github.com/gansidui/gotcp/tree/master/examples/echo) |
||||||
|
* [telnet](https://github.com/gansidui/gotcp/tree/master/examples/telnet) |
||||||
|
|
||||||
|
Document |
||||||
|
================ |
||||||
|
|
||||||
|
[Doc](http://godoc.org/github.com/gansidui/gotcp) |
@ -0,0 +1,215 @@ |
|||||||
|
package gotcp |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"net" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Error type
|
||||||
|
var ( |
||||||
|
ErrConnClosing = errors.New("use of closed network connection") |
||||||
|
ErrWriteBlocking = errors.New("write packet was blocking") |
||||||
|
ErrReadBlocking = errors.New("read packet was blocking") |
||||||
|
) |
||||||
|
|
||||||
|
// Conn exposes a set of callbacks for the various events that occur on a connection
|
||||||
|
type Conn struct { |
||||||
|
srv *Server |
||||||
|
conn *net.TCPConn // the raw connection
|
||||||
|
extraData interface{} // to save extra data
|
||||||
|
closeOnce sync.Once // close the conn, once, per instance
|
||||||
|
closeFlag int32 // close flag
|
||||||
|
closeChan chan struct{} // close chanel
|
||||||
|
packetSendChan chan Packet // packet send chanel
|
||||||
|
packetReceiveChan chan Packet // packeet receive chanel
|
||||||
|
} |
||||||
|
|
||||||
|
// ConnCallback is an interface of methods that are used as callbacks on a connection
|
||||||
|
type ConnCallback interface { |
||||||
|
// OnConnect is called when the connection was accepted,
|
||||||
|
// If the return value of false is closed
|
||||||
|
OnConnect(*Conn) bool |
||||||
|
|
||||||
|
// OnMessage is called when the connection receives a packet,
|
||||||
|
// If the return value of false is closed
|
||||||
|
OnMessage(*Conn, Packet) bool |
||||||
|
|
||||||
|
// OnClose is called when the connection closed
|
||||||
|
OnClose(*Conn) |
||||||
|
} |
||||||
|
|
||||||
|
// newConn returns a wrapper of raw conn
|
||||||
|
func newConn(conn *net.TCPConn, srv *Server) *Conn { |
||||||
|
return &Conn{ |
||||||
|
srv: srv, |
||||||
|
conn: conn, |
||||||
|
closeChan: make(chan struct{}), |
||||||
|
packetSendChan: make(chan Packet, srv.config.PacketSendChanLimit), |
||||||
|
packetReceiveChan: make(chan Packet, srv.config.PacketReceiveChanLimit), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// GetExtraData gets the extra data from the Conn
|
||||||
|
func (c *Conn) GetExtraData() interface{} { |
||||||
|
return c.extraData |
||||||
|
} |
||||||
|
|
||||||
|
// PutExtraData puts the extra data with the Conn
|
||||||
|
func (c *Conn) PutExtraData(data interface{}) { |
||||||
|
c.extraData = data |
||||||
|
} |
||||||
|
|
||||||
|
// GetRawConn returns the raw net.TCPConn from the Conn
|
||||||
|
func (c *Conn) GetRawConn() *net.TCPConn { |
||||||
|
return c.conn |
||||||
|
} |
||||||
|
|
||||||
|
// Close closes the connection
|
||||||
|
func (c *Conn) Close() { |
||||||
|
c.closeOnce.Do(func() { |
||||||
|
atomic.StoreInt32(&c.closeFlag, 1) |
||||||
|
close(c.closeChan) |
||||||
|
close(c.packetSendChan) |
||||||
|
close(c.packetReceiveChan) |
||||||
|
c.conn.Close() |
||||||
|
c.srv.callback.OnClose(c) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// IsClosed indicates whether or not the connection is closed
|
||||||
|
func (c *Conn) IsClosed() bool { |
||||||
|
return atomic.LoadInt32(&c.closeFlag) == 1 |
||||||
|
} |
||||||
|
|
||||||
|
// AsyncWritePacket async writes a packet, this method will never block
|
||||||
|
func (c *Conn) AsyncWritePacket(p Packet, timeout time.Duration) (err error) { |
||||||
|
if c.IsClosed() { |
||||||
|
return ErrConnClosing |
||||||
|
} |
||||||
|
|
||||||
|
defer func() { |
||||||
|
if e := recover(); e != nil { |
||||||
|
err = ErrConnClosing |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
if timeout == 0 { |
||||||
|
select { |
||||||
|
case c.packetSendChan <- p: |
||||||
|
return nil |
||||||
|
|
||||||
|
default: |
||||||
|
return ErrWriteBlocking |
||||||
|
} |
||||||
|
|
||||||
|
} else { |
||||||
|
select { |
||||||
|
case c.packetSendChan <- p: |
||||||
|
return nil |
||||||
|
|
||||||
|
case <-c.closeChan: |
||||||
|
return ErrConnClosing |
||||||
|
|
||||||
|
case <-time.After(timeout): |
||||||
|
return ErrWriteBlocking |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Do it
|
||||||
|
func (c *Conn) Do() { |
||||||
|
if !c.srv.callback.OnConnect(c) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
asyncDo(c.handleLoop, c.srv.waitGroup) |
||||||
|
asyncDo(c.readLoop, c.srv.waitGroup) |
||||||
|
asyncDo(c.writeLoop, c.srv.waitGroup) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Conn) readLoop() { |
||||||
|
defer func() { |
||||||
|
recover() |
||||||
|
c.Close() |
||||||
|
}() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-c.srv.exitChan: |
||||||
|
return |
||||||
|
|
||||||
|
case <-c.closeChan: |
||||||
|
return |
||||||
|
|
||||||
|
default: |
||||||
|
} |
||||||
|
|
||||||
|
p, err := c.srv.protocol.ReadPacket(c.conn) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
c.packetReceiveChan <- p |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Conn) writeLoop() { |
||||||
|
defer func() { |
||||||
|
recover() |
||||||
|
c.Close() |
||||||
|
}() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-c.srv.exitChan: |
||||||
|
return |
||||||
|
|
||||||
|
case <-c.closeChan: |
||||||
|
return |
||||||
|
|
||||||
|
case p := <-c.packetSendChan: |
||||||
|
if c.IsClosed() { |
||||||
|
return |
||||||
|
} |
||||||
|
if _, err := c.conn.Write(p.Serialize()); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Conn) handleLoop() { |
||||||
|
defer func() { |
||||||
|
recover() |
||||||
|
c.Close() |
||||||
|
}() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-c.srv.exitChan: |
||||||
|
return |
||||||
|
|
||||||
|
case <-c.closeChan: |
||||||
|
return |
||||||
|
|
||||||
|
case p := <-c.packetReceiveChan: |
||||||
|
if c.IsClosed() { |
||||||
|
return |
||||||
|
} |
||||||
|
if !c.srv.callback.OnMessage(c, p) { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func asyncDo(fn func(), wg *sync.WaitGroup) { |
||||||
|
wg.Add(1) |
||||||
|
go func() { |
||||||
|
fn() |
||||||
|
wg.Done() |
||||||
|
}() |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gansidui/gotcp/examples/echo" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", "127.0.0.1:8989") |
||||||
|
checkError(err) |
||||||
|
conn, err := net.DialTCP("tcp", nil, tcpAddr) |
||||||
|
checkError(err) |
||||||
|
|
||||||
|
echoProtocol := &echo.EchoProtocol{} |
||||||
|
|
||||||
|
// ping <--> pong
|
||||||
|
for i := 0; i < 3; i++ { |
||||||
|
// write
|
||||||
|
conn.Write(echo.NewEchoPacket([]byte("hello"), false).Serialize()) |
||||||
|
|
||||||
|
// read
|
||||||
|
p, err := echoProtocol.ReadPacket(conn) |
||||||
|
if err == nil { |
||||||
|
echoPacket := p.(*echo.EchoPacket) |
||||||
|
fmt.Printf("Server reply:[%v] [%v]\n", echoPacket.GetLength(), string(echoPacket.GetBody())) |
||||||
|
} |
||||||
|
|
||||||
|
time.Sleep(2 * time.Second) |
||||||
|
} |
||||||
|
|
||||||
|
conn.Close() |
||||||
|
} |
||||||
|
|
||||||
|
func checkError(err error) { |
||||||
|
if err != nil { |
||||||
|
log.Fatal(err) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
package echo |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/binary" |
||||||
|
"errors" |
||||||
|
"io" |
||||||
|
"net" |
||||||
|
|
||||||
|
"github.com/gansidui/gotcp" |
||||||
|
) |
||||||
|
|
||||||
|
type EchoPacket struct { |
||||||
|
buff []byte |
||||||
|
} |
||||||
|
|
||||||
|
func (this *EchoPacket) Serialize() []byte { |
||||||
|
return this.buff |
||||||
|
} |
||||||
|
|
||||||
|
func (this *EchoPacket) GetLength() uint32 { |
||||||
|
return binary.BigEndian.Uint32(this.buff[0:4]) |
||||||
|
} |
||||||
|
|
||||||
|
func (this *EchoPacket) GetBody() []byte { |
||||||
|
return this.buff[4:] |
||||||
|
} |
||||||
|
|
||||||
|
func NewEchoPacket(buff []byte, hasLengthField bool) *EchoPacket { |
||||||
|
p := &EchoPacket{} |
||||||
|
|
||||||
|
if hasLengthField { |
||||||
|
p.buff = buff |
||||||
|
|
||||||
|
} else { |
||||||
|
p.buff = make([]byte, 4+len(buff)) |
||||||
|
binary.BigEndian.PutUint32(p.buff[0:4], uint32(len(buff))) |
||||||
|
copy(p.buff[4:], buff) |
||||||
|
} |
||||||
|
|
||||||
|
return p |
||||||
|
} |
||||||
|
|
||||||
|
type EchoProtocol struct { |
||||||
|
} |
||||||
|
|
||||||
|
func (this *EchoProtocol) ReadPacket(conn *net.TCPConn) (gotcp.Packet, error) { |
||||||
|
var ( |
||||||
|
lengthBytes []byte = make([]byte, 4) |
||||||
|
length uint32 |
||||||
|
) |
||||||
|
|
||||||
|
// read length
|
||||||
|
if _, err := io.ReadFull(conn, lengthBytes); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if length = binary.BigEndian.Uint32(lengthBytes); length > 1024 { |
||||||
|
return nil, errors.New("the size of packet is larger than the limit") |
||||||
|
} |
||||||
|
|
||||||
|
buff := make([]byte, 4+length) |
||||||
|
copy(buff[0:4], lengthBytes) |
||||||
|
|
||||||
|
// read body ( buff = lengthBytes + body )
|
||||||
|
if _, err := io.ReadFull(conn, buff[4:]); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
return NewEchoPacket(buff, true), nil |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
"runtime" |
||||||
|
"syscall" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gansidui/gotcp" |
||||||
|
"github.com/gansidui/gotcp/examples/echo" |
||||||
|
) |
||||||
|
|
||||||
|
type Callback struct{} |
||||||
|
|
||||||
|
func (this *Callback) OnConnect(c *gotcp.Conn) bool { |
||||||
|
addr := c.GetRawConn().RemoteAddr() |
||||||
|
c.PutExtraData(addr) |
||||||
|
fmt.Println("OnConnect:", addr) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (this *Callback) OnMessage(c *gotcp.Conn, p gotcp.Packet) bool { |
||||||
|
echoPacket := p.(*echo.EchoPacket) |
||||||
|
fmt.Printf("OnMessage:[%v] [%v]\n", echoPacket.GetLength(), string(echoPacket.GetBody())) |
||||||
|
c.AsyncWritePacket(echo.NewEchoPacket(echoPacket.Serialize(), true), time.Second) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (this *Callback) OnClose(c *gotcp.Conn) { |
||||||
|
fmt.Println("OnClose:", c.GetExtraData()) |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU()) |
||||||
|
|
||||||
|
// creates a tcp listener
|
||||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", ":8989") |
||||||
|
checkError(err) |
||||||
|
listener, err := net.ListenTCP("tcp", tcpAddr) |
||||||
|
checkError(err) |
||||||
|
|
||||||
|
// creates a server
|
||||||
|
config := &gotcp.Config{ |
||||||
|
PacketSendChanLimit: 20, |
||||||
|
PacketReceiveChanLimit: 20, |
||||||
|
} |
||||||
|
srv := gotcp.NewServer(config, &Callback{}, &echo.EchoProtocol{}) |
||||||
|
|
||||||
|
// starts service
|
||||||
|
go srv.Start(listener, time.Second) |
||||||
|
fmt.Println("listening:", listener.Addr()) |
||||||
|
|
||||||
|
// catchs system signal
|
||||||
|
chSig := make(chan os.Signal) |
||||||
|
signal.Notify(chSig, syscall.SIGINT, syscall.SIGTERM) |
||||||
|
fmt.Println("Signal: ", <-chSig) |
||||||
|
|
||||||
|
// stops service
|
||||||
|
srv.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
func checkError(err error) { |
||||||
|
if err != nil { |
||||||
|
log.Fatal(err) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"os" |
||||||
|
"os/signal" |
||||||
|
"runtime" |
||||||
|
"syscall" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/gansidui/gotcp" |
||||||
|
"github.com/gansidui/gotcp/examples/telnet" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU()) |
||||||
|
|
||||||
|
// creates a tcp listener
|
||||||
|
tcpAddr, err := net.ResolveTCPAddr("tcp4", ":23") |
||||||
|
checkError(err) |
||||||
|
listener, err := net.ListenTCP("tcp", tcpAddr) |
||||||
|
checkError(err) |
||||||
|
|
||||||
|
// creates a server
|
||||||
|
config := &gotcp.Config{ |
||||||
|
PacketSendChanLimit: 20, |
||||||
|
PacketReceiveChanLimit: 20, |
||||||
|
} |
||||||
|
srv := gotcp.NewServer(config, &telnet.TelnetCallback{}, &telnet.TelnetProtocol{}) |
||||||
|
|
||||||
|
// starts service
|
||||||
|
go srv.Start(listener, time.Second) |
||||||
|
fmt.Println("listening:", listener.Addr()) |
||||||
|
|
||||||
|
// catchs system signal
|
||||||
|
chSig := make(chan os.Signal) |
||||||
|
signal.Notify(chSig, syscall.SIGINT, syscall.SIGTERM) |
||||||
|
fmt.Println("Signal: ", <-chSig) |
||||||
|
|
||||||
|
// stops service
|
||||||
|
srv.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
func checkError(err error) { |
||||||
|
if err != nil { |
||||||
|
log.Fatal(err) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
package telnet |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/gansidui/gotcp" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
endTag = []byte("\r\n") // Telnet command's end tag
|
||||||
|
) |
||||||
|
|
||||||
|
// Packet
|
||||||
|
type TelnetPacket struct { |
||||||
|
pType string |
||||||
|
pData []byte |
||||||
|
} |
||||||
|
|
||||||
|
func (p *TelnetPacket) Serialize() []byte { |
||||||
|
buf := p.pData |
||||||
|
buf = append(buf, endTag...) |
||||||
|
return buf |
||||||
|
} |
||||||
|
|
||||||
|
func (p *TelnetPacket) GetType() string { |
||||||
|
return p.pType |
||||||
|
} |
||||||
|
|
||||||
|
func (p *TelnetPacket) GetData() []byte { |
||||||
|
return p.pData |
||||||
|
} |
||||||
|
|
||||||
|
func NewTelnetPacket(pType string, pData []byte) *TelnetPacket { |
||||||
|
return &TelnetPacket{ |
||||||
|
pType: pType, |
||||||
|
pData: pData, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type TelnetProtocol struct { |
||||||
|
} |
||||||
|
|
||||||
|
func (this *TelnetProtocol) ReadPacket(conn *net.TCPConn) (gotcp.Packet, error) { |
||||||
|
fullBuf := bytes.NewBuffer([]byte{}) |
||||||
|
for { |
||||||
|
data := make([]byte, 1024) |
||||||
|
|
||||||
|
readLengh, err := conn.Read(data) |
||||||
|
|
||||||
|
if err != nil { //EOF, or worse
|
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if readLengh == 0 { // Connection maybe closed by the client
|
||||||
|
return nil, gotcp.ErrConnClosing |
||||||
|
} else { |
||||||
|
fullBuf.Write(data[:readLengh]) |
||||||
|
|
||||||
|
index := bytes.Index(fullBuf.Bytes(), endTag) |
||||||
|
if index > -1 { |
||||||
|
command := fullBuf.Next(index) |
||||||
|
fullBuf.Next(2) |
||||||
|
//fmt.Println(string(command))
|
||||||
|
|
||||||
|
commandList := strings.Split(string(command), " ") |
||||||
|
if len(commandList) > 1 { |
||||||
|
return NewTelnetPacket(commandList[0], []byte(commandList[1])), nil |
||||||
|
} else { |
||||||
|
if commandList[0] == "quit" { |
||||||
|
return NewTelnetPacket("quit", command), nil |
||||||
|
} else { |
||||||
|
return NewTelnetPacket("unknow", command), nil |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type TelnetCallback struct { |
||||||
|
} |
||||||
|
|
||||||
|
func (this *TelnetCallback) OnConnect(c *gotcp.Conn) bool { |
||||||
|
addr := c.GetRawConn().RemoteAddr() |
||||||
|
c.PutExtraData(addr) |
||||||
|
fmt.Println("OnConnect:", addr) |
||||||
|
c.AsyncWritePacket(NewTelnetPacket("unknow", []byte("Welcome to this Telnet Server")), 0) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (this *TelnetCallback) OnMessage(c *gotcp.Conn, p gotcp.Packet) bool { |
||||||
|
packet := p.(*TelnetPacket) |
||||||
|
command := packet.GetData() |
||||||
|
commandType := packet.GetType() |
||||||
|
|
||||||
|
switch commandType { |
||||||
|
case "echo": |
||||||
|
c.AsyncWritePacket(NewTelnetPacket("echo", command), 0) |
||||||
|
case "login": |
||||||
|
c.AsyncWritePacket(NewTelnetPacket("login", []byte(string(command)+" has login")), 0) |
||||||
|
case "quit": |
||||||
|
return false |
||||||
|
default: |
||||||
|
c.AsyncWritePacket(NewTelnetPacket("unknow", []byte("unknow command")), 0) |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (this *TelnetCallback) OnClose(c *gotcp.Conn) { |
||||||
|
fmt.Println("OnClose:", c.GetExtraData()) |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
package gotcp |
||||||
|
|
||||||
|
import ( |
||||||
|
"net" |
||||||
|
) |
||||||
|
|
||||||
|
type Packet interface { |
||||||
|
Serialize() []byte |
||||||
|
} |
||||||
|
|
||||||
|
type Protocol interface { |
||||||
|
ReadPacket(conn *net.TCPConn) (Packet, error) |
||||||
|
} |
@ -0,0 +1,68 @@ |
|||||||
|
package gotcp |
||||||
|
|
||||||
|
import ( |
||||||
|
"net" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
PacketSendChanLimit uint32 // the limit of packet send channel
|
||||||
|
PacketReceiveChanLimit uint32 // the limit of packet receive channel
|
||||||
|
} |
||||||
|
|
||||||
|
type Server struct { |
||||||
|
config *Config // server configuration
|
||||||
|
callback ConnCallback // message callbacks in connection
|
||||||
|
protocol Protocol // customize packet protocol
|
||||||
|
exitChan chan struct{} // notify all goroutines to shutdown
|
||||||
|
waitGroup *sync.WaitGroup // wait for all goroutines
|
||||||
|
} |
||||||
|
|
||||||
|
// NewServer creates a server
|
||||||
|
func NewServer(config *Config, callback ConnCallback, protocol Protocol) *Server { |
||||||
|
return &Server{ |
||||||
|
config: config, |
||||||
|
callback: callback, |
||||||
|
protocol: protocol, |
||||||
|
exitChan: make(chan struct{}), |
||||||
|
waitGroup: &sync.WaitGroup{}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Start starts service
|
||||||
|
func (s *Server) Start(listener *net.TCPListener, acceptTimeout time.Duration) { |
||||||
|
s.waitGroup.Add(1) |
||||||
|
defer func() { |
||||||
|
listener.Close() |
||||||
|
s.waitGroup.Done() |
||||||
|
}() |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-s.exitChan: |
||||||
|
return |
||||||
|
|
||||||
|
default: |
||||||
|
} |
||||||
|
|
||||||
|
listener.SetDeadline(time.Now().Add(acceptTimeout)) |
||||||
|
|
||||||
|
conn, err := listener.AcceptTCP() |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
s.waitGroup.Add(1) |
||||||
|
go func() { |
||||||
|
newConn(conn, s).Do() |
||||||
|
s.waitGroup.Done() |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Stop stops service
|
||||||
|
func (s *Server) Stop() { |
||||||
|
close(s.exitChan) |
||||||
|
s.waitGroup.Wait() |
||||||
|
} |
Loading…
Reference in new issue