Project ui, ws

- [wip] ws support
This commit is contained in:
Matej Kramny 2016-04-02 13:40:07 +01:00
parent 121567fa03
commit 79a05b389f
40 changed files with 751 additions and 17 deletions

View File

@ -6,8 +6,8 @@ create table user (
`email` varchar(255) not null comment "Email, unique",
`password` varchar(255) not null comment "Password",
UNIQUE KEY `username` (`username`),
UNIQUE KEY `email` (`email`)
unique key `username` (`username`),
unique key `email` (`email`)
) ENGINE=InnoDB CHARSET=utf8;
create table project (
@ -21,6 +21,91 @@ create table project__user (
`user_id` varchar (255) not null comment "User ID",
`admin` tinyint (1) not null default 0 comment 'Gives user god-like privileges',
UNIQUE KEY `id` (`project_id`, `user_id`)
unique key `id` (`project_id`, `user_id`),
foreign key (`project_id`) references project(`id`) on delete cascade,
foreign key (`user_id`) references user(`id`) on delete cascade
) ENGINE=InnoDB CHARSET=utf8;
create table access_key (
`id` int(11) not null primary key auto_increment,
`name` varchar(255) not null,
`type` varchar(255) not null comment 'aws/do/gcloud',
`project_id` int(11) null,
`key` text null,
`secret` text null,
foreign key (`project_id`) references project(`id`) on delete set null
) ENGINE=InnoDB CHARSET=utf8;
create table project__repository (
`id` int(11) not null primary key auto_increment,
`project_id` int(11) not null,
`git_url` text not null,
`ssh_key_id` int(11) not null,
foreign key (`project_id`) references project(`id`) on delete cascade,
foreign key (`ssh_key_id`) references access_key(`id`)
) ENGINE=InnoDB CHARSET=utf8;
create table project__inventory (
`id` int(11) not null primary key auto_increment,
`project_id` int(11) not null,
`type` varchar(255) not null comment 'can be static/aws/do/gcloud',
`key_id` int(11) not null comment 'references keys to authenticate remote services',
`inventory` longtext not null,
foreign key (`project_id`) references project(`id`) on delete cascade,
foreign key (`key_id`) references access_key(`id`)
) ENGINE=InnoDB CHARSET=utf8;
create table project__environment (
`id` int(11) not null primary key auto_increment,
`project_id` int(11) not null,
`password` varchar(255) null,
`json` longtext not null,
foreign key (`project_id`) references project(`id`) on delete cascade
) ENGINE=InnoDB CHARSET=utf8;
create table project__template (
`id` int(11) not null primary key auto_increment,
`ssh_key_id` int(11) not null comment 'for accessing the inventory',
`project_id` int(11) not null,
`inventory_id` int(11) not null,
`repository_id` int(11) not null,
`environment_id` int(11) not null,
`playbook` varchar(255) not null comment 'playbook name (ansible.yml)',
foreign key (`project_id`) references project(`id`) on delete cascade,
foreign key (`ssh_key_id`) references access_key(`id`),
foreign key (`inventory_id`) references project__inventory(`id`),
foreign key (`repository_id`) references project__repository(`id`),
foreign key (`environment_id`) references project__environment(`id`)
) ENGINE=InnoDB CHARSET=utf8;
create table project__template_schedule (
`template_id` int(11) not null,
`cron_format` varchar(255) not null,
foreign key (`template_id`) references project__template(`id`) on delete cascade
) ENGINE=InnoDB CHARSET=utf8;
create table task (
`id` int(11) not null primary key auto_increment,
`template_id` int(11) not null,
`status` varchar(255) not null,
`playbook` varchar(255) not null comment 'override playbook name (ansible.yml)',
`environment` longtext null comment 'override environment',
foreign key (`template_id`) references project__template(`id`)
) ENGINE=InnoDB CHARSET=utf8;
create table task__output (
`task_id` int(11) not null,
`time` datetime not null default NOW(),
`output` longtext not null,
unique key `id` (`task_id`, `time`),
foreign key (`task_id`) references task(`id`) on delete cascade
) ENGINE=InnoDB CHARSET=utf8;

View File

@ -6,6 +6,7 @@ import (
"github.com/ansible-semaphore/semaphore/database"
"github.com/ansible-semaphore/semaphore/migration"
"github.com/ansible-semaphore/semaphore/routes"
"github.com/ansible-semaphore/semaphore/routes/sockets"
"github.com/ansible-semaphore/semaphore/util"
"github.com/bugsnag/bugsnag-go"
"github.com/gin-gonic/gin"
@ -29,6 +30,7 @@ func main() {
return
}
go sockets.StartWS()
r := gin.New()
r.Use(gin.Recovery(), recovery, gin.Logger())

30
models/Project.go Normal file
View File

@ -0,0 +1,30 @@
package models
import (
"time"
"github.com/ansible-semaphore/semaphore/database"
)
type Project struct {
ID int `json:"id"`
Name string `json:"name" binding:"required"`
Created time.Time `json:"created"`
}
func (project *Project) CreateProject() error {
res, err := database.Mysql.Exec("insert into project set name=?", project.Name)
if err != nil {
return err
}
projectID, err := res.LastInsertId()
if err != nil {
return err
}
project.ID = int(projectID)
project.Created = time.Now()
return nil
}

View File

@ -12,7 +12,7 @@ type User struct {
Username string `db:"username" json:"username"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
Password string `db:"password" json:"password"`
Password string `db:"password" json:"-"`
}
func FetchUser(userID int) (*User, error) {

View File

@ -10,10 +10,13 @@
"app",
"routes/router",
"routes/auth",
"routes/project",
"routes/identities",
"routes/playbooks",
"routes/users",
"factories/project",
"/vendor/require.min",
"../require_config"
]

View File

@ -6,6 +6,6 @@
.col-md-4
.panel.panel-default
.panel-heading Projects
button.btn.btn-default.btn-xs.pull-right: i.fa.fa-fw.fa-plus
button.btn.btn-default.btn-xs.pull-right(ng-click="addProject()"): i.fa.fa-fw.fa-plus
ul.list-group
li.list-group-item(ng-repeat="project in projects") {{ project.name }}
li.list-group-item(ng-repeat="project in projects"): a(ui-sref="project.dashboard({ project_id: project.id })") {{ project.name }}

View File

@ -12,7 +12,13 @@ html(lang="en" ng-app="semaphore")
body
.navbar.navbar-default(ng-if="loggedIn"): .container-fluid
.navbar-header
a.navbar-brand(ui-sref="homepage") SEMAPHORE
a.navbar-brand(ui-sref="dashboard") SEMAPHORE
ul.nav.navbar-nav
li(ng-class="{ active: $state.includes('dashboard') }")
a(ui-sref="dashboard") Dashboard
li(ng-class="{ active: $state.includes('keys') }")
a(ui-sref="keys") Key Store
ul.nav.navbar-nav.navbar-right(style="margin-right: 0;")
li(uib-dropdown)

View File

@ -0,0 +1,13 @@
.modal-header
h4.modal-title Add Project
.modal-body
form.form-horizontal
.form-group
label.control-label.col-sm-4 Name
.col-sm-6
input.form-control(type="text" placeholder="Project Name" ng-model="project.name" required)
.modal-footer
button.btn.btn-default.pull-left(ng-click="$dismiss()") Dismiss
button.btn.btn-success(ng-click="$close(project)") Save

View File

@ -0,0 +1,14 @@
.container-fluid: .row
.col-sm-4.col-md-3
h3.no-top-margin {{ project.name }}
ul.list-group
li.list-group-item: a(ui-sref="project.dashboard") Dashboard
li.list-group-item: a(ui-sref="project.templates") Task Templates
li.list-group-item: a(ui-sref="project.inventory") Inventory
li.list-group-item: a(ui-sref="project.environment") Environment
li.list-group-item: a(ui-sref="project.keys") Key Store
li.list-group-item: a(ui-sref="project.repositories") Playbook Repositories
li.list-group-item: a(ui-sref="project.schedule") Task Schedule
li.list-group-item: a(ui-sref="project.users") Team
.col-sm-8.col-md-9
ui-view

View File

@ -0,0 +1 @@
h4 Project activity

View File

View File

View File

View File

View File

View File

View File

@ -0,0 +1,8 @@
h4 Users
table.table
tbody: tr(ng-repeat="user in users")
td {{ user.name }}
td {{ user.username }}
td {{ user.email }}
td: button.btn.btn-default.pull-right.btn-xs(ng-click="remove(user)") remove

View File

@ -11,7 +11,7 @@ app.config(['$httpProvider', function ($httpProvider) {
request.url = url = url.replace('/tpl/', '/public/html/');
}
if (!(url.indexOf('/public') !== -1 || url.indexOf('://') !== -1)) {
if (!(url.indexOf('/public') !== -1 || url.indexOf('://') !== -1 || url.indexOf('uib/template') !== -1)) {
request.url = "/api" + request.url;
request.headers['Cache-Control'] = 'no-cache';
}
@ -50,14 +50,35 @@ app.run(['$rootScope', '$window', '$couchPotato', '$injector', '$state', '$http'
$rootScope.user = null;
$rootScope.loggedIn = false;
$rootScope.ws = null;
$http.get('/user')
.then(function (user) {
$rootScope.user = user;
$rootScope.loggedIn = true;
$rootScope.startWS();
}, function () {
$state.go('auth.login');
});
}
$rootScope.startWS = function () {
var ws_proto = 'ws:';
if (document.location.protocol == 'https:') {
ws_proto = 'wss:';
}
$rootScope.ws = new WebSocket(ws_proto + '//' + document.location.host + '/api/ws');
$rootScope.ws.onclose = function () {
console.log('WS closed, retrying');
setTimeout($rootScope.startWS, 2000);
}
$rootScope.ws.onmessage = function (e) {
console.log('msg', e);
}
}
$rootScope.refreshUser();
}]);

View File

@ -1,9 +1,21 @@
define(function () {
app.registerController('DashboardCtrl', function ($scope, $http) {
$scope.projects = [{
name: 'Hey there'
}, {
name: 'Test project'
}];
})
})
define(['controllers/projects/edit'], function () {
app.registerController('DashboardCtrl', ['$scope', '$http', '$uibModal', function ($scope, $http, $modal) {
$scope.projects = [];
$http.get('/projects').success(function (projects) {
$scope.projects = projects;
})
$scope.addProject = function () {
$modal.open({
templateUrl: '/tpl/projects/add.html'
}).result.then(function (project) {
$http.post('/projects', project)
.success(function () {
}).error(function (data, status) {
swal('Error', 'Could not create project: ' + status, 'error');
});
});
}
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectDashboardCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectEditCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectEnvironmentCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectInventoryCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectKeysCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectRepositoriesCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectScheduleCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,4 @@
define(function () {
app.registerController('ProjectTemplatesCtrl', ['$scope', '$http', function ($scope, $http) {
}]);
});

View File

@ -0,0 +1,19 @@
define(function () {
app.registerController('ProjectUsersCtrl', ['$scope', '$http', 'Project', function ($scope, $http, Project) {
$scope.reload = function () {
$http.get(Project.getURL() + '/users').success(function (users) {
$scope.users = users;
});
}
$scope.remove = function (user) {
$http.delete(Project.getURL() + '/users/' + user.id).success(function () {
$scope.reload();
}).error(function () {
swal('error', 'could not delete user..', 'error');
});
}
$scope.reload();
}]);
});

View File

@ -0,0 +1,12 @@
app.factory('ProjectFactory', ['$http', function ($http) {
var Project = function (project) {
this.id = project.id;
this.name = project.name;
}
Project.prototype.getURL = function () {
return '/project/' + this.id;
}
return Project;
}]);

104
public/js/routes/project.js Normal file
View File

@ -0,0 +1,104 @@
app.config(function ($stateProvider, $couchPotatoProvider) {
$stateProvider
.state('project', {
url: '/project/:project_id',
abstract: true,
templateUrl: '/tpl/projects/container.html',
controller: function ($scope, Project) {
$scope.project = Project;
},
resolve: {
Project: ['$http', '$stateParams', '$q', 'ProjectFactory', function ($http, params, $q, ProjectFactory) {
var d = $q.defer();
$http.get('/project/' + params.project_id)
.success(function (project) {
d.resolve(new ProjectFactory(project));
}).error(function () {
d.resolve(false);
});
return d.promise;
}]
}
})
.state('project.dashboard', {
url: '',
pageTitle: 'Project Dashboard',
templateUrl: '/tpl/projects/dashboard.html',
controller: 'ProjectDashboardCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/dashboard'])
}
})
.state('project.users', {
url: '/users',
pageTitle: 'Users',
templateUrl: '/tpl/projects/users.html',
controller: 'ProjectUsersCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/users'])
}
})
.state('project.templates', {
url: '/templates',
pageTitle: 'Templates',
templateUrl: '/tpl/projects/templates.html',
controller: 'ProjectTemplatesCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/templates'])
}
})
.state('project.inventory', {
url: '/inventory',
pageTitle: 'Inventory',
templateUrl: '/tpl/projects/inventory.html',
controller: 'ProjectInventoryCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/inventory'])
}
})
.state('project.environment', {
url: '/environment',
pageTitle: 'Environment',
templateUrl: '/tpl/projects/environment.html',
controller: 'ProjectEnvironmentCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/environment'])
}
})
.state('project.keys', {
url: '/keys',
pageTitle: 'Keys',
templateUrl: '/tpl/projects/keys.html',
controller: 'ProjectKeysCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/keys'])
}
})
.state('project.repositories', {
url: '/repositories',
pageTitle: 'Repositories',
templateUrl: '/tpl/projects/repositories.html',
controller: 'ProjectRepositoriesCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/repositories'])
}
})
.state('project.schedule', {
url: '/schedule',
pageTitle: 'Template Schedule',
templateUrl: '/tpl/projects/schedule.html',
controller: 'ProjectScheduleCtrl',
resolve: {
$d: $couchPotatoProvider.resolveDependencies(['controllers/projects/schedule'])
}
});
});

View File

@ -0,0 +1,12 @@
package projects
import "github.com/gin-gonic/gin"
func GetProjectEnvironment(c *gin.Context) {
}
func AddProjectEnvironment(c *gin.Context) {
}
func RemoveProjectEnvironment(c *gin.Context) {
}

View File

@ -0,0 +1,12 @@
package projects
import "github.com/gin-gonic/gin"
func GetProjectInventories(c *gin.Context) {
}
func AddProjectInventory(c *gin.Context) {
}
func RemoveProjectInventory(c *gin.Context) {
}

12
routes/projects/keys.go Normal file
View File

@ -0,0 +1,12 @@
package projects
import "github.com/gin-gonic/gin"
func GetProjectKeys(c *gin.Context) {
}
func AddProjectKey(c *gin.Context) {
}
func RemoveProjectKey(c *gin.Context) {
}

View File

@ -0,0 +1,44 @@
package projects
import (
"database/sql"
"github.com/ansible-semaphore/semaphore/database"
"github.com/ansible-semaphore/semaphore/models"
"github.com/ansible-semaphore/semaphore/util"
"github.com/gin-gonic/gin"
"github.com/masterminds/squirrel"
)
func ProjectMiddleware(c *gin.Context) {
user := c.MustGet("user").(*models.User)
projectID, err := util.GetIntParam("project_id", c)
if err != nil {
return
}
query, args, _ := squirrel.Select("p.*").
From("project as p").
Join("project__user as pu on pu.project_id=p.id").
Where("p.id=?", projectID).
Where("pu.user_id=?", user.ID).
ToSql()
var project models.Project
if err := database.Mysql.SelectOne(&project, query, args...); err != nil {
if err == sql.ErrNoRows {
c.AbortWithStatus(404)
return
}
panic(err)
}
c.Set("project", project)
c.Next()
}
func GetProject(c *gin.Context) {
c.JSON(200, c.MustGet("project"))
}

View File

@ -0,0 +1,46 @@
package projects
import (
"github.com/ansible-semaphore/semaphore/database"
"github.com/ansible-semaphore/semaphore/models"
"github.com/gin-gonic/gin"
"github.com/masterminds/squirrel"
)
func GetProjects(c *gin.Context) {
user := c.MustGet("user").(*models.User)
query, args, _ := squirrel.Select("p.*").
From("project as p").
Join("project__user as pu on pu.project_id=p.id").
Where("pu.user_id=?", user.ID).
OrderBy("p.name").
ToSql()
var projects []models.Project
if _, err := database.Mysql.Select(&projects, query, args...); err != nil {
panic(err)
}
c.JSON(200, projects)
}
func AddProject(c *gin.Context) {
var body models.Project
user := c.MustGet("user").(*models.User)
if err := c.Bind(&body); err != nil {
return
}
err := body.CreateProject()
if err != nil {
panic(err)
}
if _, err := database.Mysql.Exec("insert into project__user set project_id=?, user_id=?, admin=1", body.ID, user.ID); err != nil {
panic(err)
}
c.JSON(201, body)
}

View File

@ -0,0 +1,12 @@
package projects
import "github.com/gin-gonic/gin"
func GetProjectRepositories(c *gin.Context) {
}
func AddProjectRepository(c *gin.Context) {
}
func RemoveProjectRepository(c *gin.Context) {
}

31
routes/projects/users.go Normal file
View File

@ -0,0 +1,31 @@
package projects
import (
"github.com/ansible-semaphore/semaphore/database"
"github.com/ansible-semaphore/semaphore/models"
"github.com/gin-gonic/gin"
"github.com/masterminds/squirrel"
)
func GetProjectUsers(c *gin.Context) {
project := c.MustGet("project").(models.Project)
var users []models.User
query, args, _ := squirrel.Select("u.*").
From("project__user as pu").
Join("user as u on pu.user_id=u.id").
Where("pu.project_id=?", project.ID).
ToSql()
if _, err := database.Mysql.Select(&users, query, args...); err != nil {
panic(err)
}
c.JSON(200, users)
}
func AddProjectUser(c *gin.Context) {
}
func RemoveProjectUser(c *gin.Context) {
}

View File

@ -4,6 +4,8 @@ import (
"strings"
"github.com/ansible-semaphore/semaphore/routes/auth"
"github.com/ansible-semaphore/semaphore/routes/projects"
"github.com/ansible-semaphore/semaphore/routes/sockets"
"github.com/ansible-semaphore/semaphore/util"
"github.com/gin-gonic/gin"
)
@ -28,8 +30,43 @@ func Route(r *gin.Engine) {
api.Use(MustAuthenticate)
api.GET("/ws", sockets.Handler)
api.GET("/user", getUser)
// api.PUT("/user", misc.UpdateUser)
api.GET("/projects", projects.GetProjects)
api.POST("/projects", projects.AddProject)
func(api *gin.RouterGroup) {
api.Use(projects.ProjectMiddleware)
api.GET("", projects.GetProject)
api.GET("/users", projects.GetProjectUsers)
api.POST("/users", projects.AddProjectUser)
api.DELETE("/users/:user_id", projects.RemoveProjectUser)
api.GET("/keys", projects.GetProjectKeys)
api.POST("/keys", projects.AddProjectKey)
api.DELETE("/keys", projects.RemoveProjectKey)
api.GET("/repositories", projects.GetProjectRepositories)
api.POST("/repositories", projects.AddProjectRepository)
api.DELETE("/repositories/:user_id", projects.RemoveProjectRepository)
api.GET("/inventory", projects.GetProjectInventories)
api.POST("/inventory", projects.AddProjectInventory)
api.DELETE("/inventory/:user_id", projects.RemoveProjectInventory)
api.GET("/environment", projects.GetProjectEnvironment)
api.POST("/environment", projects.AddProjectEnvironment)
api.DELETE("/environment/:user_id", projects.RemoveProjectEnvironment)
api.GET("/templates", projects.GetProjectUsers)
api.POST("/templates", projects.AddProjectUser)
api.DELETE("/templates/:user_id", projects.RemoveProjectUser)
}(api.Group("/project/:project_id"))
}
func servePublic(c *gin.Context) {

115
routes/sockets/handler.go Normal file
View File

@ -0,0 +1,115 @@
package sockets
import (
"fmt"
"log"
"time"
"github.com/ansible-semaphore/semaphore/models"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
type connection struct {
ws *websocket.Conn
send chan []byte
userID int
}
// readPump pumps messages from the websocket connection to the hub.
func (c *connection) readPump() {
defer func() {
h.unregister <- c
c.ws.Close()
}()
c.ws.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error {
c.ws.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, message, err := c.ws.ReadMessage()
fmt.Println(string(message))
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Printf("error: %v", err)
}
break
}
h.broadcast <- message
}
}
// write writes a message with the given message type and payload.
func (c *connection) write(mt int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(mt, payload)
}
// writePump pumps messages from the hub to the websocket connection.
func (c *connection) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.ws.Close()
}()
for {
select {
case message, ok := <-c.send:
if !ok {
c.write(websocket.CloseMessage, []byte{})
return
}
if err := c.write(websocket.TextMessage, message); err != nil {
return
}
case <-ticker.C:
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
return
}
}
}
}
func Handler(context *gin.Context) {
user := context.MustGet("user").(*models.User)
ws, err := upgrader.Upgrade(context.Writer, context.Request, nil)
if err != nil {
panic(err)
}
c := &connection{
send: make(chan []byte, 256),
ws: ws,
userID: user.ID,
}
h.register <- c
go c.writePump()
c.readPump()
}

51
routes/sockets/pool.go Normal file
View File

@ -0,0 +1,51 @@
package sockets
// hub maintains the set of active connections and broadcasts messages to the
// connections.
type hub struct {
// Registered connections.
connections map[*connection]bool
// Inbound messages from the connections.
broadcast chan []byte
// Register requests from the connections.
register chan *connection
// Unregister requests from connections.
unregister chan *connection
}
var h = hub{
broadcast: make(chan []byte),
register: make(chan *connection),
unregister: make(chan *connection),
connections: make(map[*connection]bool),
}
func (h *hub) run() {
for {
select {
case c := <-h.register:
h.connections[c] = true
case c := <-h.unregister:
if _, ok := h.connections[c]; ok {
delete(h.connections, c)
close(c.send)
}
case m := <-h.broadcast:
for c := range h.connections {
select {
case c.send <- m:
default:
close(c.send)
delete(h.connections, c)
}
}
}
}
}
func StartWS() {
h.run()
}