diff options
36 files changed, 555 insertions, 198 deletions
@@ -128,3 +128,5 @@ Visual Studio Code launcher settings can be found into `.vscode/launch.json`. - replace makefile by build.go to make Windows build support easier - add more tests - add more documentation +- add authentication / login (oauth) + HTTPS +- enable syncthing user/password + HTTPS diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go index 2df8ea7..7fa69e9 100644 --- a/lib/apiv1/apiv1.go +++ b/lib/apiv1/apiv1.go @@ -50,10 +50,8 @@ func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolder * s.apiRouter.POST("/make", s.buildMake) s.apiRouter.POST("/make/:id", s.buildMake) - /* TODO: to be tested and then enabled s.apiRouter.POST("/exec", s.execCmd) s.apiRouter.POST("/exec/:id", s.execCmd) - */ return s } diff --git a/lib/apiv1/exec.go b/lib/apiv1/exec.go index 18fdc7e..895807d 100644 --- a/lib/apiv1/exec.go +++ b/lib/apiv1/exec.go @@ -12,10 +12,12 @@ import ( // ExecArgs JSON parameters of /exec command type ExecArgs struct { - ID string `json:"id"` - RPath string `json:"rpath"` // relative path into project + ID string `json:"id" binding:"required"` + SdkID string `json:"sdkid"` // sdk ID to use for setting env Cmd string `json:"cmd" binding:"required"` Args []string `json:"args"` + Env []string `json:"env"` + RPath string `json:"rpath"` // relative path into project CmdTimeout int `json:"timeout"` // command completion timeout in Second } @@ -51,7 +53,7 @@ func (s *APIService) execCmd(c *gin.Context) { return } - // TODO: add permission + // TODO: add permission ? // Retrieve session info sess := s.sessions.Get(c) @@ -89,14 +91,23 @@ func (s *APIService) execCmd(c *gin.Context) { // Define callback for output var oCB common.EmitOutputCB - oCB = func(sid string, id int, stdout, stderr string) { + oCB = func(sid string, id int, stdout, stderr string, data *map[string]interface{}) { // IO socket can be nil when disconnected so := s.sessions.IOSocketGet(sid) if so == nil { s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id) return } - s.log.Debugf("%s emitted - WS sid %s - id:%d", ExecOutEvent, sid, id) + + // Retrieve project ID and RootPath + prjID := (*data)["ID"].(string) + prjRootPath := (*data)["RootPath"].(string) + + // Cleanup any references to internal rootpath in stdout & stderr + stdout = strings.Replace(stdout, prjRootPath, "", -1) + stderr = strings.Replace(stderr, prjRootPath, "", -1) + + s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", ExecOutEvent, sid, id, prjID) // FIXME replace by .BroadcastTo a room err := (*so).Emit(ExecOutEvent, ExecOutMsg{ @@ -135,14 +146,26 @@ func (s *APIService) execCmd(c *gin.Context) { cmdID := execCommandID execCommandID++ + cmd := []string{} - cmd := "cd " + prj.GetFullPath(args.RPath) + " && " + args.Cmd + // Setup env var regarding Sdk ID (used for example to setup cross toolchain) + if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 { + cmd = append(cmd, envCmd...) + cmd = append(cmd, "&&") + } + + cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", args.Cmd) if len(args.Args) > 0 { - cmd += " " + strings.Join(args.Args, " ") + cmd = append(cmd, args.Args...) } - s.log.Debugf("Execute [Cmd ID %d]: %v %v", cmdID, cmd) - err := common.ExecPipeWs(cmd, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB) + s.log.Debugf("Execute [Cmd ID %d]: %v", cmdID, cmd) + + data := make(map[string]interface{}) + data["ID"] = prj.ID + data["RootPath"] = prj.RootPath + + err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, &data) if err != nil { common.APIError(c, err.Error()) return diff --git a/lib/apiv1/make.go b/lib/apiv1/make.go index fb6435e..098e41c 100644 --- a/lib/apiv1/make.go +++ b/lib/apiv1/make.go @@ -13,11 +13,12 @@ import ( // MakeArgs is the parameters (json format) of /make command type MakeArgs struct { - ID string `json:"id"` - RPath string `json:"rpath"` // relative path into project - Args string `json:"args"` // args to pass to make command - SdkID string `json:"sdkid"` // sdk ID to use for setting env - CmdTimeout int `json:"timeout"` // command completion timeout in Second + ID string `json:"id"` + SdkID string `json:"sdkid"` // sdk ID to use for setting env + Args []string `json:"args"` // args to pass to make command + Env []string `json:"env"` + RPath string `json:"rpath"` // relative path into project + CmdTimeout int `json:"timeout"` // command completion timeout in Second } // MakeOutMsg Message send on each output (stdout+stderr) of make command @@ -85,14 +86,9 @@ func (s *APIService) buildMake(c *gin.Context) { execTmo = 24 * 60 * 60 // 1 day } - cmd := "cd " + prj.GetFullPath(args.RPath) + " && make" - if args.Args != "" { - cmd += " " + args.Args - } - // Define callback for output var oCB common.EmitOutputCB - oCB = func(sid string, id int, stdout, stderr string) { + oCB = func(sid string, id int, stdout, stderr string, data *map[string]interface{}) { // IO socket can be nil when disconnected so := s.sessions.IOSocketGet(sid) if so == nil { @@ -138,14 +134,21 @@ func (s *APIService) buildMake(c *gin.Context) { cmdID := makeCommandID makeCommandID++ + cmd := []string{} // Retrieve env command regarding Sdk ID - if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); envCmd != "" { - cmd = envCmd + " && " + cmd + if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 { + cmd = append(cmd, envCmd...) + cmd = append(cmd, "&&") + } + + cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", "make") + if len(args.Args) > 0 { + cmd = append(cmd, args.Args...) } s.log.Debugf("Execute [Cmd ID %d]: %v", cmdID, cmd) - err := common.ExecPipeWs(cmd, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB) + err := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, nil) if err != nil { common.APIError(c, err.Error()) return diff --git a/lib/common/execPipeWs.go b/lib/common/execPipeWs.go index 3b63cdc..4994d9d 100644 --- a/lib/common/execPipeWs.go +++ b/lib/common/execPipeWs.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "time" "syscall" @@ -14,7 +15,7 @@ import ( ) // EmitOutputCB is the function callback used to emit data -type EmitOutputCB func(sid string, cmdID int, stdout, stderr string) +type EmitOutputCB func(sid string, cmdID int, stdout, stderr string, data *map[string]interface{}) // EmitExitCB is the function callback used to emit exit proc code type EmitExitCB func(sid string, cmdID int, code int, err error) @@ -23,8 +24,8 @@ type EmitExitCB func(sid string, cmdID int, code int, err error) // https://github.com/gorilla/websocket/blob/master/examples/command/main.go // ExecPipeWs executes a command and redirect stdout/stderr into a WebSocket -func ExecPipeWs(cmd string, so *socketio.Socket, sid string, cmdID int, - cmdExecTimeout int, log *logrus.Logger, eoCB EmitOutputCB, eeCB EmitExitCB) error { +func ExecPipeWs(cmd []string, env []string, so *socketio.Socket, sid string, cmdID int, + cmdExecTimeout int, log *logrus.Logger, eoCB EmitOutputCB, eeCB EmitExitCB, data *map[string]interface{}) error { outr, outw, err := os.Pipe() if err != nil { @@ -39,9 +40,10 @@ func ExecPipeWs(cmd string, so *socketio.Socket, sid string, cmdID int, return fmt.Errorf("Pipe stdin error: " + err.Error()) } - bashArgs := []string{"/bin/bash", "-c", cmd} + bashArgs := []string{"/bin/bash", "-c", strings.Join(cmd, " ")} proc, err := os.StartProcess("/bin/bash", bashArgs, &os.ProcAttr{ Files: []*os.File{inr, outw, outw}, + Env: append(os.Environ(), env...), }) if err != nil { outr.Close() @@ -58,7 +60,7 @@ func ExecPipeWs(cmd string, so *socketio.Socket, sid string, cmdID int, defer inw.Close() stdoutDone := make(chan struct{}) - go cmdPumpStdout(so, outr, stdoutDone, sid, cmdID, log, eoCB) + go cmdPumpStdout(so, outr, stdoutDone, sid, cmdID, log, eoCB, data) // Blocking function that poll input or wait for end of process cmdPumpStdin(so, inw, proc, sid, cmdID, cmdExecTimeout, log, eeCB) @@ -133,13 +135,13 @@ func cmdPumpStdin(so *socketio.Socket, w io.Writer, proc *os.Process, } func cmdPumpStdout(so *socketio.Socket, r io.Reader, done chan struct{}, - sid string, cmdID int, log *logrus.Logger, emitFuncCB EmitOutputCB) { + sid string, cmdID int, log *logrus.Logger, emitFuncCB EmitOutputCB, data *map[string]interface{}) { defer func() { }() sc := bufio.NewScanner(r) for sc.Scan() { - emitFuncCB(sid, cmdID, string(sc.Bytes()), "") + emitFuncCB(sid, cmdID, string(sc.Bytes()), "", data) } if sc.Err() != nil { log.Errorln("scan:", sc.Err()) diff --git a/lib/crosssdk/sdk.go b/lib/crosssdk/sdk.go index 9aeec90..5a5770d 100644 --- a/lib/crosssdk/sdk.go +++ b/lib/crosssdk/sdk.go @@ -48,6 +48,6 @@ func NewCrossSDK(path string) (*SDK, error) { } // GetEnvCmd returns the command used to initialized the environment -func (s *SDK) GetEnvCmd() string { - return ". " + s.EnvFile +func (s *SDK) GetEnvCmd() []string { + return []string{"source", s.EnvFile} } diff --git a/lib/crosssdk/sdks.go b/lib/crosssdk/sdks.go index abfef82..d08afc5 100644 --- a/lib/crosssdk/sdks.go +++ b/lib/crosssdk/sdks.go @@ -71,15 +71,15 @@ func (s *SDKs) Get(id int) SDK { } // GetEnvCmd returns the command used to initialized the environment for an SDK -func (s *SDKs) GetEnvCmd(id string, defaultID string) string { +func (s *SDKs) GetEnvCmd(id string, defaultID string) []string { if id == "" && defaultID == "" { // no env cmd - return "" + return []string{} } s.mutex.Lock() defer s.mutex.Unlock() - defaultEnv := "" + defaultEnv := []string{} for _, sdk := range s.Sdks { if sdk.ID == id { return sdk.GetEnvCmd() diff --git a/lib/webserver/server.go b/lib/webserver/server.go index 774195c..4268b40 100644 --- a/lib/webserver/server.go +++ b/lib/webserver/server.go @@ -154,12 +154,10 @@ func (s *Server) middlewareXDSDetails() gin.HandlerFunc { // CORS middleware func (s *Server) middlewareCORS() gin.HandlerFunc { return func(c *gin.Context) { - if c.Request.Method == "OPTIONS" { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Headers", "Content-Type") - c.Header("Access-Control-Allow-Methods", "POST, DELETE, GET, PUT") - c.Header("Content-Type", "application/json") + c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE") c.Header("Access-Control-Max-Age", cookieMaxAge) c.AbortWithStatus(204) return @@ -183,7 +183,7 @@ func xdsApp(cliCtx *cli.Context) error { relativePath = stFld.RawPath } - newFld := xdsconfig.NewFolderConfig(stFld.ID, stFld.Label, ctx.Config.ShareRootDir, strings.Trim(relativePath, "/"), defaultSdk) + newFld := xdsconfig.NewFolderConfig(stFld.ID, stFld.Label, ctx.Config.ShareRootDir, strings.TrimRight(relativePath, "/"), defaultSdk) ctx.Config.Folders = ctx.Config.Folders.Update(xdsconfig.FoldersConfig{newFld}) } diff --git a/webapp/src/app/alert/alert.component.ts b/webapp/src/app/alert/alert.component.ts index 449506f..672d7bf 100644 --- a/webapp/src/app/alert/alert.component.ts +++ b/webapp/src/app/alert/alert.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { Observable } from 'rxjs'; -import {AlertService, IAlert} from '../common/alert.service'; +import {AlertService, IAlert} from '../services/alert.service'; @Component({ selector: 'app-alert', diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html index ab792be..3dc77ef 100644 --- a/webapp/src/app/app.component.html +++ b/webapp/src/app/app.component.html @@ -6,7 +6,7 @@ <div class="navbar-collapse collapse menu2"> <ul class="nav navbar-nav navbar-right"> - <li><a routerLink="/build"><i class="fa fa-2x fa-play-circle" title="Open build page"></i></a></li> + <li><a routerLink="/devel"><i class="fa fa-2x fa-play-circle" title="Open build page"></i></a></li> <li><a routerLink="/config"><i class="fa fa-2x fa-cog" title="Open configuration page"></i></a></li> <li><a routerLink="/home"><i class="fa fa-2x fa-home" title="Back to home page"></i></a></li> </ul> diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index 1abcf0c..d02cdf2 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -24,14 +24,16 @@ import { SdksListAccordionComponent } from "./sdks/sdksListAccordion.component"; import { SdkSelectDropdownComponent } from "./sdks/sdkSelectDropdown.component"; import { HomeComponent } from "./home/home.component"; -import { BuildComponent } from "./build/build.component"; -import { XDSServerService } from "./common/xdsserver.service"; -import { XDSAgentService } from "./common/xdsagent.service"; -import { SyncthingService } from "./common/syncthing.service"; -import { ConfigService } from "./common/config.service"; -import { AlertService } from './common/alert.service'; -import { UtilsService } from './common/utils.service'; -import { SdkService } from "./common/sdk.service"; +import { DevelComponent } from "./devel/devel.component"; +import { BuildComponent } from "./devel/build/build.component"; +import { DeployComponent } from "./devel/deploy/deploy.component"; +import { XDSServerService } from "./services/xdsserver.service"; +import { XDSAgentService } from "./services/xdsagent.service"; +import { SyncthingService } from "./services/syncthing.service"; +import { ConfigService } from "./services/config.service"; +import { AlertService } from './services/alert.service'; +import { UtilsService } from './services/utils.service'; +import { SdkService } from "./services/sdk.service"; @@ -54,6 +56,8 @@ import { SdkService } from "./common/sdk.service"; AlertComponent, HomeComponent, BuildComponent, + DevelComponent, + DeployComponent, ConfigComponent, ProjectCardComponent, ProjectReadableTypePipe, diff --git a/webapp/src/app/app.routing.ts b/webapp/src/app/app.routing.ts index 747727c..f0d808f 100644 --- a/webapp/src/app/app.routing.ts +++ b/webapp/src/app/app.routing.ts @@ -2,7 +2,7 @@ import {Routes, RouterModule} from "@angular/router"; import {ModuleWithProviders} from "@angular/core"; import {ConfigComponent} from "./config/config.component"; import {HomeComponent} from "./home/home.component"; -import {BuildComponent} from "./build/build.component"; +import {DevelComponent} from "./devel/devel.component"; const appRoutes: Routes = [ @@ -10,7 +10,7 @@ const appRoutes: Routes = [ {path: 'config', component: ConfigComponent, data: {title: 'Config'}}, {path: 'home', component: HomeComponent, data: {title: 'Home'}}, - {path: 'build', component: BuildComponent, data: {title: 'Build'}} + {path: 'devel', component: DevelComponent, data: {title: 'Build & Deploy'}} ]; export const AppRoutingProviders: any[] = []; diff --git a/webapp/src/app/build/build.component.html b/webapp/src/app/build/build.component.html deleted file mode 100644 index 3d866f3..0000000 --- a/webapp/src/app/build/build.component.html +++ /dev/null @@ -1,71 +0,0 @@ -<form [formGroup]="buildForm"> - <div class="col-xs-12"> - <table class="table table-borderless"> - <tbody> - <tr> - <th style="border: none;">Project</th> - <td> - <div class="btn-group" dropdown *ngIf="curProject"> - <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;"> - {{curProject.label}} <span class="caret" style="float: right; margin-top: 8px;"></span> - </button> - <ul *dropdownMenu class="dropdown-menu" role="menu"> - <li role="menuitem"><a class="dropdown-item" *ngFor="let prj of (config$ | async)?.projects" (click)="curProject=prj"> - {{prj.label}}</a> - </li> - </ul> - </div> - </td> - </tr> - <tr> - <th>Cross SDK</th> - <td> - <!-- FIXME why not working ? - <sdk-select-dropdown [sdks]="(sdks$ | async)"></sdk-select-dropdown> - --> - <sdk-select-dropdown></sdk-select-dropdown> - </td> - </tr> - <tr> - <th>Sub-directory</th> - <td> <input type="text" style="width:99%;" formControlName="subpath"> </td> - </tr> - <tr> - <th>Make arguments</th> - <td> <input type="text" style="width:99%;" formControlName="makeArgs"> </td> - </tr> - </tbody> - </table> - </div> - <div class="row"> - <div class="col-xs-12 text-center"> - <div class="btn-group blocks"> - <button class="btn btn-primary btn-large" (click)="make() " [disabled]="!confValid ">Build</button> - <button class="btn btn-primary btn-large" (click)="make('clean') " [disabled]="!confValid ">Clean</button> - </div> - </div> - </div> -</form> - -<div style="margin-left: 2em; margin-right: 2em; "> - <div class="row "> - <div class="col-xs-10"> - <div class="row "> - <div class="col-xs-4"> - <label>Command Output</label> - </div> - <div class="col-xs-8" style="font-size:x-small; margin-top:5px;"> - {{ cmdInfo }} - </div> - </div> - </div> - <div class="col-xs-2"> - <button class="btn btn-link pull-right " (click)="reset() "><span class="fa fa-eraser fa-size-x2"></span></button> - </div> - </div> - <div class="row "> - <div class="col-xs-12 text-center "> - <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea> - </div> - </div> -</div> diff --git a/webapp/src/app/config/config.component.ts b/webapp/src/app/config/config.component.ts index 1e1e9c2..c6b2573 100644 --- a/webapp/src/app/config/config.component.ts +++ b/webapp/src/app/config/config.component.ts @@ -7,12 +7,12 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/debounceTime'; -import { ConfigService, IConfig, IProject, ProjectType } from "../common/config.service"; -import { XDSServerService, IServerStatus, IXDSAgentInfo } from "../common/xdsserver.service"; -import { XDSAgentService, IAgentStatus } from "../common/xdsagent.service"; -import { SyncthingService, ISyncThingStatus } from "../common/syncthing.service"; -import { AlertService } from "../common/alert.service"; -import { ISdk, SdkService } from "../common/sdk.service"; +import { ConfigService, IConfig, IProject, ProjectType } from "../services/config.service"; +import { XDSServerService, IServerStatus, IXDSAgentInfo } from "../services/xdsserver.service"; +import { XDSAgentService, IAgentStatus } from "../services/xdsagent.service"; +import { SyncthingService, ISyncThingStatus } from "../services/syncthing.service"; +import { AlertService } from "../services/alert.service"; +import { ISdk, SdkService } from "../services/sdk.service"; @Component({ templateUrl: './app/config/config.component.html', diff --git a/webapp/src/app/build/build.component.css b/webapp/src/app/devel/build/build.component.css index 11784db..6784a9f 100644 --- a/webapp/src/app/build/build.component.css +++ b/webapp/src/app/devel/build/build.component.css @@ -10,6 +10,12 @@ border-radius: 4px !important; } +.table-center { + width: 80%; + margin-left: auto; + margin-right: auto; +} + .table-borderless>tbody>tr>td, .table-borderless>tbody>tr>th, .table-borderless>tfoot>tr>td, @@ -30,4 +36,10 @@ .textarea-scroll { width: 100%; overflow-y: scroll; +} + +h2 { + font-family: sans-serif; + font-variant: small-caps; + font-size: x-large; }
\ No newline at end of file diff --git a/webapp/src/app/devel/build/build.component.html b/webapp/src/app/devel/build/build.component.html new file mode 100644 index 0000000..f4be204 --- /dev/null +++ b/webapp/src/app/devel/build/build.component.html @@ -0,0 +1,74 @@ +<div class="panel panel-default"> + <div class="panel-heading"> + <h2 class="panel-title">Build</h2> + </div> + <div class="panel-body"> + <form [formGroup]="buildForm"> + <div class="col-xs-12"> + <table class="table table-borderless table-center"> + <tbody> + <tr> + <th>Cross SDK</th> + <td> + <!-- FIXME why not working ? + <sdk-select-dropdown [sdks]="(sdks$ | async)"></sdk-select-dropdown> + --> + <sdk-select-dropdown></sdk-select-dropdown> + </td> + </tr> + <tr> + <th>Project root path</th> + <td> <input type="text" disabled style="width:99%;" [value]="curProject && curProject.path"></td> + </tr> + <tr> + <th>Sub-path</th> + <td> <input type="text" style="width:99%;" formControlName="subpath"> </td> + </tr> + <tr> + <th>Command arguments</th> + <td> <input type="text" style="width:99%;" formControlName="cmdArgs"> </td> + </tr> + <tr> + <th>Env variables</th> + <td> <input type="text" style="width:99%;" formControlName="envVars"> </td> + </tr> + </tbody> + </table> + </div> + <div class="row"> + <div class="col-xs-12 text-center"> + <div class="btn-group blocks"> + <button class="btn btn-primary btn-large" (click)="preBuild()" [disabled]="!curProject">Pre-Build</button> + <button class="btn btn-primary btn-large" (click)="build()" [disabled]="!curProject">Build</button> + <button class="btn btn-primary btn-large" (click)="populate()" [disabled]="!curProject ">Populate</button> + <button *ngIf="debugEnable" class="btn btn-primary btn-large" (click)="execCmd()" [disabled]="!curProject ">Execute command</button> + <button *ngIf="debugEnable" class="btn btn-primary btn-large" (click)="make()" [disabled]="!curProject ">Make</button> + </div> + </div> + </div> + </form> + + <div style="margin-left: 2em; margin-right: 2em; "> + <div class="row "> + <div class="col-xs-10"> + <div class="row "> + <div class="col-xs-4"> + <label>Command Output</label> + </div> + <div class="col-xs-8" style="font-size:x-small; margin-top:5px;"> + {{ cmdInfo }} + </div> + </div> + </div> + <div class="col-xs-2"> + <button class="btn btn-link pull-right " (click)="reset() "><span class="fa fa-eraser fa-size-x2"></span></button> + </div> + </div> + <div class="row "> + <div class="col-xs-12 text-center "> + <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea> + </div> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/webapp/src/app/build/build.component.ts b/webapp/src/app/devel/build/build.component.ts index 17e545f..b7003b1 100644 --- a/webapp/src/app/build/build.component.ts +++ b/webapp/src/app/devel/build/build.component.ts @@ -1,17 +1,18 @@ -import { Component, AfterViewChecked, ElementRef, ViewChild, OnInit } from '@angular/core'; +import { Component, AfterViewChecked, ElementRef, ViewChild, OnInit, Input } from '@angular/core'; import { Observable } from 'rxjs'; import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms'; +import { CookieService } from 'ngx-cookie'; import 'rxjs/add/operator/scan'; import 'rxjs/add/operator/startWith'; -import { XDSServerService, ICmdOutput } from "../common/xdsserver.service"; -import { ConfigService, IConfig, IProject } from "../common/config.service"; -import { AlertService, IAlert } from "../common/alert.service"; -import { SdkService } from "../common/sdk.service"; +import { XDSServerService, ICmdOutput } from "../../services/xdsserver.service"; +import { ConfigService, IConfig, IProject } from "../../services/config.service"; +import { AlertService, IAlert } from "../../services/alert.service"; +import { SdkService } from "../../services/sdk.service"; @Component({ - selector: 'build', + selector: 'panel-build', moduleId: module.id, templateUrl: './build.component.html', styleUrls: ['./build.component.css'] @@ -20,46 +21,34 @@ import { SdkService } from "../common/sdk.service"; export class BuildComponent implements OnInit, AfterViewChecked { @ViewChild('scrollOutput') private scrollContainer: ElementRef; - config$: Observable<IConfig>; + @Input() curProject: IProject; buildForm: FormGroup; subpathCtrl = new FormControl("", Validators.required); + debugEnable: boolean = false; public cmdOutput: string; - public confValid: boolean; - public curProject: IProject; public cmdInfo: string; private startTime: Map<string, number> = new Map<string, number>(); - // I initialize the app component. constructor(private configSvr: ConfigService, private xdsSvr: XDSServerService, private fb: FormBuilder, private alertSvr: AlertService, - private sdkSvr: SdkService + private sdkSvr: SdkService, + private cookie: CookieService, ) { this.cmdOutput = ""; - this.confValid = false; this.cmdInfo = ""; // TODO: to be remove (only for debug) this.buildForm = fb.group({ subpath: this.subpathCtrl, - makeArgs: ["", Validators.nullValidator], + cmdArgs: ["", Validators.nullValidator], + envVars: ["", Validators.nullValidator], }); } ngOnInit() { - this.config$ = this.configSvr.conf; - this.config$.subscribe((cfg) => { - if ("projects" in cfg) { - this.curProject = cfg.projects[0]; - this.confValid = (cfg.projects.length && this.curProject.id != null); - } else { - this.curProject = null; - this.confValid = false; - } - }); - // Command output data tunneling this.xdsSvr.CmdOutput$.subscribe(data => { this.cmdOutput += data.stdout + "\n"; @@ -78,6 +67,9 @@ export class BuildComponent implements OnInit, AfterViewChecked { }); this._scrollToBottom(); + + // only use for debug + this.debugEnable = (this.cookie.get("debug_build") !== ""); } ngAfterViewChecked() { @@ -88,6 +80,69 @@ export class BuildComponent implements OnInit, AfterViewChecked { this.cmdOutput = ''; } + preBuild() { + this._exec( + "mkdir -p build && cd build && cmake ..", + this.buildForm.value.subpath, + [], + this.buildForm.value.envVars); + } + + build() { + this._exec( + "cd build && make", + this.buildForm.value.subpath, + this.buildForm.value.cmdArgs, + this.buildForm.value.envVars + ); + } + + populate() { + this._exec( + "SEB_TODO_script_populate", + this.buildForm.value.subpath, + [], // args + this.buildForm.value.envVars + ); + } + + execCmd() { + this._exec( + this.buildForm.value.cmdArgs, + this.buildForm.value.subpath, + [], + this.buildForm.value.envVars + ); + } + + private _exec(cmd: string, dir: string, args: string[], env: string) { + if (!this.curProject) { + this.alertSvr.warning('No active project', true); + } + + let prjID = this.curProject.id; + + this.cmdOutput += this._outputHeader(); + + let sdkid = this.sdkSvr.getCurrentId(); + + // Detect key=value in env string to build array of string + let envArr = []; + env.split(';').forEach(v => envArr.push(v.trim())); + + let t0 = performance.now(); + this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0; + + this.xdsSvr.exec(prjID, dir, cmd, sdkid, args, envArr) + .subscribe(res => { + this.startTime.set(String(res.cmdID), t0); + }, + err => { + this.cmdInfo = 'Last command duration: ' + this._computeTime(t0); + this.alertSvr.error('ERROR: ' + err); + }); + } + make(args: string) { if (!this.curProject) { this.alertSvr.warning('No active project', true); @@ -99,12 +154,16 @@ export class BuildComponent implements OnInit, AfterViewChecked { let sdkid = this.sdkSvr.getCurrentId(); - let cmdArgs = args ? args : this.buildForm.value.makeArgs; + let argsArr = args ? args.split(' ') : this.buildForm.value.cmdArgs.split(' '); + + // Detect key=value in env string to build array of string + let envArr = []; + this.buildForm.value.envVars.split(';').forEach(v => envArr.push(v.trim())); let t0 = performance.now(); this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0; - this.xdsSvr.make(prjID, this.buildForm.value.subpath, cmdArgs, sdkid) + this.xdsSvr.make(prjID, this.buildForm.value.subpath, sdkid, argsArr, envArr) .subscribe(res => { this.startTime.set(String(res.cmdID), t0); }, diff --git a/webapp/src/app/devel/deploy/deploy.component.css b/webapp/src/app/devel/deploy/deploy.component.css new file mode 100644 index 0000000..c1b39d8 --- /dev/null +++ b/webapp/src/app/devel/deploy/deploy.component.css @@ -0,0 +1,45 @@ +.vcenter { + display: inline-block; + vertical-align: middle; +} + +.blocks .btn-primary { + margin-left: 5px; + margin-right: 5px; + margin-top: 5px; + border-radius: 4px !important; +} + +.table-center { + width: 99%; + margin-left: auto; + margin-right: auto; +} + +.table-borderless>tbody>tr>td, +.table-borderless>tbody>tr>th, +.table-borderless>tfoot>tr>td, +.table-borderless>tfoot>tr>th, +.table-borderless>thead>tr>td, +.table-borderless>thead>tr>th { + border: none; +} + +.btn-large { + width: 10em; +} + +.fa-size-x2 { + font-size: 18px; +} + +.textarea-scroll { + width: 100%; + overflow-y: scroll; +} + +h2 { + font-family: sans-serif; + font-variant: small-caps; + font-size: x-large; +}
\ No newline at end of file diff --git a/webapp/src/app/devel/deploy/deploy.component.html b/webapp/src/app/devel/deploy/deploy.component.html new file mode 100644 index 0000000..7a15fa6 --- /dev/null +++ b/webapp/src/app/devel/deploy/deploy.component.html @@ -0,0 +1,31 @@ +<div class="panel panel-default"> + <div class="panel-heading"> + <h2 class="panel-title">Deployment</h2> + </div> + <div class="panel-body"> + <form [formGroup]="deployForm"> + <div class="col-xs-12"> + <table class="table table-borderless table-center"> + <tbody> + <tr> + <th>Board IP</th> + <td> <input type="text" style="width:99%;" formControlName="boardIP" placeholder="1.2.3.4"> </td> + </tr> + <tr> + <th>File to deploy</th> + <td> <input type="text" style="width:99%;" formControlName="wgtFile"> </td> + </tr> + </tbody> + </table> + </div> + <div class="row"> + <div class="col-xs-12 text-center"> + <div class="btn-group blocks"> + <button class="btn btn-primary btn-large" (click)="deploy()" [disabled]="!curProject ">Deploy</button> + </div> + </div> + </div> + </form> + + </div> +</div>
\ No newline at end of file diff --git a/webapp/src/app/devel/deploy/deploy.component.ts b/webapp/src/app/devel/deploy/deploy.component.ts new file mode 100644 index 0000000..4dba256 --- /dev/null +++ b/webapp/src/app/devel/deploy/deploy.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { Observable } from 'rxjs'; +import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms'; + +import 'rxjs/add/operator/scan'; +import 'rxjs/add/operator/startWith'; + +import { XDSAgentService, IXDSDeploy } from "../../services/xdsagent.service"; +import { ConfigService, IConfig, IProject } from "../../services/config.service"; +import { AlertService, IAlert } from "../../services/alert.service"; +import { SdkService } from "../../services/sdk.service"; + +@Component({ + selector: 'panel-deploy', + moduleId: module.id, + templateUrl: './deploy.component.html', + styleUrls: ['./deploy.component.css'] +}) + +export class DeployComponent implements OnInit { + + @Input() curProject: IProject; + + deploying: boolean; + deployForm: FormGroup; + + constructor(private configSvr: ConfigService, + private xdsAgent: XDSAgentService, + private fb: FormBuilder, + private alert: AlertService, + ) { + this.deployForm = fb.group({ + boardIP: ["", Validators.nullValidator], + wgtFile: ["", Validators.nullValidator], + }); + } + + ngOnInit() { + this.deploying = false; + if (this.curProject && this.curProject.path) { + this.deployForm.patchValue({ wgtFile: this.curProject.path }); + } + } + + deploy() { + this.deploying = true; + + this.xdsAgent.deploy( + { + boardIP: this.deployForm.value.boardIP, + file: this.deployForm.value.wgtFile + } + ).subscribe(res => { + this.deploying = false; + }, err => { + this.deploying = false; + let msg = '<span>ERROR while deploying "' + this.deployForm.value.wgtFile + '"<br>'; + msg += err; + msg += '</span>'; + this.alert.error(msg); + }); + } +}
\ No newline at end of file diff --git a/webapp/src/app/devel/devel.component.css b/webapp/src/app/devel/devel.component.css new file mode 100644 index 0000000..40d6fec --- /dev/null +++ b/webapp/src/app/devel/devel.component.css @@ -0,0 +1,14 @@ +.table-center { + width: 60%; + margin-left: auto; + margin-right: auto; +} + +.table-borderless>tbody>tr>td, +.table-borderless>tbody>tr>th, +.table-borderless>tfoot>tr>td, +.table-borderless>tfoot>tr>th, +.table-borderless>thead>tr>td, +.table-borderless>thead>tr>th { + border: none; +} diff --git a/webapp/src/app/devel/devel.component.html b/webapp/src/app/devel/devel.component.html new file mode 100644 index 0000000..5950f51 --- /dev/null +++ b/webapp/src/app/devel/devel.component.html @@ -0,0 +1,36 @@ +<div class="row"> + <div class="col-md-8"> + <table class="table table-borderless table-center"> + <tbody> + <tr> + <th style="border: none;">Project</th> + <td> + <div class="btn-group" dropdown *ngIf="curPrj"> + <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;"> + {{curPrj.label}} <span class="caret" style="float: right; margin-top: 8px;"></span> + </button> + <ul *dropdownMenu class="dropdown-menu" role="menu"> + <li role="menuitem"><a class="dropdown-item" *ngFor="let prj of (config$ | async)?.projects" (click)="curPrj=prj">{{prj.label}}</a> + </li> + </ul> + </div> + <span *ngIf="!curPrj" style="color:red; font-style: italic;"> + No project detected, please create first a project using the configuration page. + </span> + </td> + </tr> + </tbody> + </table> + </div> +</div> + +<div class="row"> + <div class="col-md-8"> + <panel-build [curProject]=curPrj></panel-build> + </div> + <!-- TODO: disable for now + <div class="col-md-4"> + <panel-deploy [curProject]=curPrj></panel-deploy> + </div> + --> +</div>
\ No newline at end of file diff --git a/webapp/src/app/devel/devel.component.ts b/webapp/src/app/devel/devel.component.ts new file mode 100644 index 0000000..ff12127 --- /dev/null +++ b/webapp/src/app/devel/devel.component.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; + +import { Observable } from 'rxjs'; + +import { ConfigService, IConfig, IProject } from "../services/config.service"; + +@Component({ + selector: 'devel', + moduleId: module.id, + templateUrl: './devel.component.html', + styleUrls: ['./devel.component.css'], +}) + +export class DevelComponent { + + curPrj: IProject; + config$: Observable<IConfig>; + + constructor(private configSvr: ConfigService) { + } + + ngOnInit() { + this.config$ = this.configSvr.conf; + this.config$.subscribe((cfg) => { + if ("projects" in cfg) { + this.curPrj = cfg.projects[0]; + } else { + this.curPrj = null; + } + }); + } +}
\ No newline at end of file diff --git a/webapp/src/app/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts index 010b476..7a7fa21 100644 --- a/webapp/src/app/projects/projectCard.component.ts +++ b/webapp/src/app/projects/projectCard.component.ts @@ -1,5 +1,5 @@ import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ConfigService, IProject, ProjectType } from "../common/config.service"; +import { ConfigService, IProject, ProjectType } from "../services/config.service"; @Component({ selector: 'project-card', diff --git a/webapp/src/app/projects/projectsListAccordion.component.ts b/webapp/src/app/projects/projectsListAccordion.component.ts index bea3f0f..1b43cea 100644 --- a/webapp/src/app/projects/projectsListAccordion.component.ts +++ b/webapp/src/app/projects/projectsListAccordion.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from "@angular/core"; -import { IProject } from "../common/config.service"; +import { IProject } from "../services/config.service"; @Component({ selector: 'projects-list-accordion', diff --git a/webapp/src/app/sdks/sdkCard.component.ts b/webapp/src/app/sdks/sdkCard.component.ts index f5d2a54..579d224 100644 --- a/webapp/src/app/sdks/sdkCard.component.ts +++ b/webapp/src/app/sdks/sdkCard.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { ISdk } from "../common/sdk.service"; +import { ISdk } from "../services/sdk.service"; @Component({ selector: 'sdk-card', diff --git a/webapp/src/app/sdks/sdkSelectDropdown.component.ts b/webapp/src/app/sdks/sdkSelectDropdown.component.ts index f213db0..a2fe37a 100644 --- a/webapp/src/app/sdks/sdkSelectDropdown.component.ts +++ b/webapp/src/app/sdks/sdkSelectDropdown.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from "@angular/core"; -import { ISdk, SdkService } from "../common/sdk.service"; +import { ISdk, SdkService } from "../services/sdk.service"; @Component({ selector: 'sdk-select-dropdown', diff --git a/webapp/src/app/sdks/sdksListAccordion.component.ts b/webapp/src/app/sdks/sdksListAccordion.component.ts index 9094f27..9d5f7e9 100644 --- a/webapp/src/app/sdks/sdksListAccordion.component.ts +++ b/webapp/src/app/sdks/sdksListAccordion.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from "@angular/core"; -import { ISdk } from "../common/sdk.service"; +import { ISdk } from "../services/sdk.service"; @Component({ selector: 'sdks-list-accordion', diff --git a/webapp/src/app/common/alert.service.ts b/webapp/src/app/services/alert.service.ts index 9dab36a..9dab36a 100644 --- a/webapp/src/app/common/alert.service.ts +++ b/webapp/src/app/services/alert.service.ts diff --git a/webapp/src/app/common/config.service.ts b/webapp/src/app/services/config.service.ts index 091ee06..390340a 100644 --- a/webapp/src/app/common/config.service.ts +++ b/webapp/src/app/services/config.service.ts @@ -13,11 +13,11 @@ import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/mergeMap'; -import { XDSServerService, IXDSConfigProject } from "../common/xdsserver.service"; -import { XDSAgentService } from "../common/xdsagent.service"; -import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../common/syncthing.service"; -import { AlertService, IAlert } from "../common/alert.service"; -import { UtilsService } from "../common/utils.service"; +import { XDSServerService, IXDSConfigProject } from "../services/xdsserver.service"; +import { XDSAgentService } from "../services/xdsagent.service"; +import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../services/syncthing.service"; +import { AlertService, IAlert } from "../services/alert.service"; +import { UtilsService } from "../services/utils.service"; export enum ProjectType { NATIVE = 1, @@ -150,9 +150,27 @@ export class ConfigService { this.AgentConnectObs = this.xdsAgentSvr.connect(cfg.retry, cfg.URL) .subscribe((sts) => { //console.log("Agent sts", sts); - }, error => this.alert.error(error) - ); + // FIXME: load projects from local XDS Agent and + // not directly from local syncthing + this._loadProjectFromLocalST(); + + }, error => { + if (error.indexOf("XDS local Agent not responding") !== -1) { + let msg = "<span><strong>" + error + "<br></strong>"; + msg += "You may need to download and execute XDS-Agent.<br>"; + if (this.confStore.xdsAgentZipUrl !== "") { + msg += "<a class=\"fa fa-download\" href=\"" + this.confStore.xdsAgentZipUrl + "\" target=\"_blank\"></a>"; + msg += " Download XDS-Agent tarball."; + } + msg += "</span>"; + this.alert.error(msg); + } else { + this.alert.error(error); + } + }); + } + private _loadProjectFromLocalST() { // Remove previous subscriber if existing if (this.stConnectObs) { try { @@ -198,11 +216,7 @@ export class ConfigService { }, error => { if (error.indexOf("Syncthing local daemon not responding") !== -1) { let msg = "<span><strong>" + error + "<br></strong>"; - msg += "You may need to download and execute XDS-Agent.<br>"; - if (this.confStore.xdsAgentZipUrl !== "") { - msg += "<a class=\"fa fa-download\" href=\"" + this.confStore.xdsAgentZipUrl + "\" target=\"_blank\"></a>"; - msg += " Download XDS-Agent tarball."; - } + msg += "Please check that local XDS-Agent is running.<br>"; msg += "</span>"; this.alert.error(msg); } else { diff --git a/webapp/src/app/common/sdk.service.ts b/webapp/src/app/services/sdk.service.ts index 19c49d9..fa4cd55 100644 --- a/webapp/src/app/common/sdk.service.ts +++ b/webapp/src/app/services/sdk.service.ts @@ -2,7 +2,7 @@ import { Injectable, SecurityContext } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { XDSServerService } from "../common/xdsserver.service"; +import { XDSServerService } from "../services/xdsserver.service"; export interface ISdk { id: string; diff --git a/webapp/src/app/common/syncthing.service.ts b/webapp/src/app/services/syncthing.service.ts index 0e8c51c..0e8c51c 100644 --- a/webapp/src/app/common/syncthing.service.ts +++ b/webapp/src/app/services/syncthing.service.ts diff --git a/webapp/src/app/common/utils.service.ts b/webapp/src/app/services/utils.service.ts index 291ffd3..291ffd3 100644 --- a/webapp/src/app/common/utils.service.ts +++ b/webapp/src/app/services/utils.service.ts diff --git a/webapp/src/app/common/xdsagent.service.ts b/webapp/src/app/services/xdsagent.service.ts index 86f0336..c6c52c8 100644 --- a/webapp/src/app/common/xdsagent.service.ts +++ b/webapp/src/app/services/xdsagent.service.ts @@ -21,6 +21,11 @@ export interface IXDSVersion { } +export interface IXDSDeploy { + boardIP: string; + file: string; +} + export interface IAgentStatus { baseURL: string; connected: boolean; @@ -66,9 +71,7 @@ export class XDSAgentService { if (url) { this._initURLs(url); } - //FIXME [XDS-Agent]: not implemented yet, set always as connected - //this._status.connected = false; - this._status.connected = true; + this._status.connected = false; this._status.connectionRetry = 0; this.connectionMaxRetry = retry || 3600; // 1 hour @@ -85,14 +88,11 @@ export class XDSAgentService { } public getVersion(): Observable<IXDSVersion> { - /*FIXME [XDS-Agent]: Not implemented for now return this._get('/version'); - */ - return Observable.of({ - version: "NOT_IMPLEMENTED", - apiVersion: "NOT_IMPLEMENTED", - gitTag: "NOT_IMPLEMENTED" - }); + } + + public deploy(dpy: IXDSDeploy) { + return this._post('/deploy', dpy); } private _initURLs(url: string) { @@ -139,7 +139,6 @@ export class XDSAgentService { options = options || {}; let headers = options.headers || new Headers(); // headers.append('Authorization', 'Basic ' + btoa('username:password')); - headers.append('Access-Control-Allow-Origin', '*'); headers.append('Accept', 'application/json'); headers.append('Content-Type', 'application/json'); if (this.apikey !== "") { @@ -156,7 +155,7 @@ export class XDSAgentService { return Observable.of(true); } - return this.http.get(this._status.baseURL, this._attachAuthHeaders()) + return this.http.get(this.baseRestUrl + "/version", this._attachAuthHeaders()) .map((r) => this._status.connected = true) .retryWhen((attempts) => { this._status.connectionRetry = 0; @@ -181,9 +180,7 @@ export class XDSAgentService { return this._checkAlive() .flatMap(() => this.http.post(this.baseRestUrl + url, JSON.stringify(body), this._attachAuthHeaders())) .map((res: Response) => res.json()) - .catch((error) => { - return this._decodeError(error); - }); + .catch(this._decodeError); } private _delete(url: string): Observable<any> { return this._checkAlive() @@ -197,7 +194,13 @@ export class XDSAgentService { if (this._status) { this._status.connected = false; } - if (typeof err === "object") { + if (err instanceof Response) { + const body = err.json() || 'Server error'; + e = body.error || JSON.stringify(body); + if (!e || e === "") { + e = `${err.status} - ${err.statusText || 'Unknown error'}`; + } + } else if (typeof err === "object") { if (err.statusText) { e = err.statusText; } else if (err.error) { @@ -205,10 +208,6 @@ export class XDSAgentService { } else { e = JSON.stringify(err); } - } else if (err instanceof Response) { - const body = err.json() || 'Server error'; - const error = body.error || JSON.stringify(body); - e = `${err.status} - ${err.statusText || ''} ${error}`; } else { e = err.message ? err.message : err.toString(); } diff --git a/webapp/src/app/common/xdsserver.service.ts b/webapp/src/app/services/xdsserver.service.ts index 49c2d37..22e4ac9 100644 --- a/webapp/src/app/common/xdsserver.service.ts +++ b/webapp/src/app/services/xdsserver.service.ts @@ -147,6 +147,14 @@ export class XDSServerService { this.CmdExit$.next(Object.assign({}, <ICmdExit>data)); }); + this.socket.on('exec:output', data => { + this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data)); + }); + + this.socket.on('exec:exit', data => { + this.CmdExit$.next(Object.assign({}, <ICmdExit>data)); + }); + } getSdks(): Observable<ISdk[]> { @@ -177,16 +185,27 @@ export class XDSServerService { return this._delete('/folder/' + id); } - exec(cmd: string, args?: string[], options?: any): Observable<any> { + exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> { return this._post('/exec', { + id: prjID, + rpath: dir, cmd: cmd, - args: args || [] + sdkid: sdkid || "", + args: args || [], + env: env || [], }); } - make(prjID: string, dir: string, args: string, sdkid?: string): Observable<any> { - return this._post('/make', { id: prjID, rpath: dir, args: args, sdkid: sdkid }); + make(prjID: string, dir: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> { + return this._post('/make', + { + id: prjID, + rpath: dir, + sdkid: sdkid, + args: args || [], + env: env || [], + }); } |