Update to rc.2 | NEW: iris run main.go https://github.com/kataras/iris/issues/192

Not tested on linux yet,(I do not have a linux station now). Post an
issue if iris run main.go doesnt works as expected
This commit is contained in:
Makis Maropoulos 2016-06-20 11:59:36 +03:00
parent 39e1504ba3
commit e9a4746000
10 changed files with 336 additions and 175 deletions

View File

@ -1,5 +1,15 @@
# History # History
## 3.0.0-rc.1 -> 3.0.0-rc.2
New:
- ` iris.MustUse/MustUseFunc` - registers middleware for all route parties, all subdomains and all routes.
- iris control plugin re-written, added real time browser request logger
- `websocket.OnError` - Add OnError to be able to catch internal errors from the connection
- [command line tool](https://github.com/kataras/iris/tree/master/iris) - `iris run main.go` runs, watch and reload on source code changes. As requested [here](https://github.com/kataras/iris/issues/192)
Fixes: https://github.com/kataras/iris/issues/184 , https://github.com/kataras/iris/issues/175 .
## 3.0.0-beta.3, 3.0.0-beta.4 -> 3.0.0-rc.1 ## 3.0.0-beta.3, 3.0.0-beta.4 -> 3.0.0-rc.1
This version took me many days because the whole framework's underline code is rewritten after many many many 'yoga'. Iris is not so small anymore, so I (tried) to organized it a little better. Note that, today, you can just go to [iris.go](https://github.com/kataras/iris/tree/master/iris.go) and [context.go](https://github.com/kataras/iris/tree/master/context/context.go) and look what functions you can use. You had some 'bugs' to subdomains, mail service, basic authentication and logger, these are fixed also, see below... This version took me many days because the whole framework's underline code is rewritten after many many many 'yoga'. Iris is not so small anymore, so I (tried) to organized it a little better. Note that, today, you can just go to [iris.go](https://github.com/kataras/iris/tree/master/iris.go) and [context.go](https://github.com/kataras/iris/tree/master/context/context.go) and look what functions you can use. You had some 'bugs' to subdomains, mail service, basic authentication and logger, these are fixed also, see below...

View File

@ -6,7 +6,7 @@
[Travis]: http://travis-ci.org/kataras/iris [Travis]: http://travis-ci.org/kataras/iris
[License Widget]: https://img.shields.io/badge/license-Apache%20License%202.0-E91E63.svg?style=flat-square [License Widget]: https://img.shields.io/badge/license-Apache%20License%202.0-E91E63.svg?style=flat-square
[License]: https://github.com/kataras/iris/blob/master/LICENSE [License]: https://github.com/kataras/iris/blob/master/LICENSE
[Release Widget]: https://img.shields.io/badge/release-v3.0.0--rc.1-blue.svg?style=flat-square [Release Widget]: https://img.shields.io/badge/release-v3.0.0--rc.2-blue.svg?style=flat-square
[Release]: https://github.com/kataras/iris/releases [Release]: https://github.com/kataras/iris/releases
[Gitter Widget]: https://img.shields.io/badge/chat-on%20gitter-00BCD4.svg?style=flat-square [Gitter Widget]: https://img.shields.io/badge/chat-on%20gitter-00BCD4.svg?style=flat-square
[Gitter]: https://gitter.im/kataras/iris [Gitter]: https://gitter.im/kataras/iris
@ -124,7 +124,7 @@ Iris suggests you to use [this](https://github.com/gavv/httpexpect) new suite t
Versioning Versioning
------------ ------------
Current: **v3.0.0-rc.1** Current: **v3.0.0-rc.2**
> Iris is an active project > Iris is an active project

View File

@ -69,7 +69,7 @@ import (
const ( const (
// Version of the iris // Version of the iris
Version = "3.0.0-rc.1" Version = "3.0.0-rc.2"
banner = ` _____ _ banner = ` _____ _
|_ _| (_) |_ _| (_)
| | ____ _ ___ | | ____ _ ___

View File

@ -7,14 +7,14 @@ This package is the command line tool for [../](https://github.com/kataras/iris
[![Iris installed screen](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iris_cli_screen2.png)](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iris_cli_screen2.png) [![Iris installed screen](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iris_cli_screen2.png)](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iris_cli_screen2.png)
## Install ## Install
Current version: 0.0.4 Current version: 0.0.5
```sh ```sh
go get -u github.com/kataras/iris/iris go get -u github.com/kataras/iris/iris
``` ```
## Usage # Usage
```sh ```sh
@ -24,7 +24,7 @@ $ iris [command] [-flags]
> Note that you must have $GOPATH/bin to your $PATH system/environment variable. > Note that you must have $GOPATH/bin to your $PATH system/environment variable.
## Create ## create
**The create command** creates for you a start project in a directory **The create command** creates for you a start project in a directory
@ -57,15 +57,24 @@ iris create -d myproject
Will create the basic sample package to the `$GOPATH/src/myproject` folder and run the app. Will create the basic sample package to the `$GOPATH/src/myproject` folder and run the app.
## run
## Version **The run command** runs & reload on file changes your Iris station
It's like ` go run ` but with directory watcher and re-run on .go file changes.
```sh
iris run main.go
```
[![Iris CLI run showcase](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iris_command_line_tool_run_command.png)](https://raw.githubusercontent.com/iris-contrib/website/gh-pages/assets/iris_command_line_tool_run_command.png)
## version
```sh ```sh
iris version iris version
``` ```
Will print the current iris' installed version to your machine Will print the current Iris' installed version to your machine
## TODO
- [ ] Add more templates

131
iris/create.go Normal file
View File

@ -0,0 +1,131 @@
package main
import (
"io/ioutil"
"os"
"runtime"
"strings"
"github.com/kataras/cli"
"github.com/kataras/iris/utils"
)
const (
// PackagesURL the url to download all the packages
PackagesURL = "https://github.com/iris-contrib/iris-command-assets/archive/master.zip"
// PackagesExportedName the folder created after unzip
PackagesExportedName = "iris-command-assets-master"
)
var (
packagesInstallDir = utils.AssetsDirectory + utils.PathSeparator + "iris-command-assets" + utils.PathSeparator
)
func create(flags cli.Flags) (err error) {
if !utils.DirectoryExists(packagesInstallDir) || !flags.Bool("offline") {
downloadPackages()
}
targetDir := flags.String("dir")
// remove first and last / if any
if strings.HasPrefix(targetDir, "./") || strings.HasPrefix(targetDir, "."+utils.PathSeparator) {
targetDir = targetDir[2:]
}
if targetDir[len(targetDir)-1] == '/' {
targetDir = targetDir[0 : len(targetDir)-1]
}
//
createPackage(flags.String("type"), targetDir)
return
}
func downloadPackages() {
errMsg := "\nProblem while downloading the assets from the internet for the first time. Trace: %s"
installedDir, err := utils.Install(PackagesURL, packagesInstallDir)
if err != nil {
printer.Dangerf(errMsg, err.Error())
return
}
// installedDir is the packagesInstallDir+PackagesExportedName, we will copy these contents to the parent, to the packagesInstallDir, because of import paths.
err = utils.CopyDir(installedDir, packagesInstallDir)
if err != nil {
printer.Dangerf(errMsg, err.Error())
return
}
// we don't exit on errors here.
// try to remove the unzipped folder
utils.RemoveFile(installedDir[0 : len(installedDir)-1])
}
func createPackage(packageName string, targetDir string) error {
installTo := os.Getenv("GOPATH") + utils.PathSeparator + "src" + utils.PathSeparator + targetDir
packageDir := packagesInstallDir + utils.PathSeparator + packageName
err := utils.CopyDir(packageDir, installTo)
if err != nil {
printer.Dangerf("\nProblem while copying the %s package to the %s. Trace: %s", packageName, installTo, err.Error())
return err
}
// now replace main.go's 'github.com/iris-contrib/iris-command-assets/basic/' with targetDir
// hardcode all that, we don't have anything special and neither will do
targetDir = strings.Replace(targetDir, "\\", "/", -1) // for any case
mainFile := installTo + utils.PathSeparator + "backend" + utils.PathSeparator + "main.go"
input, err := ioutil.ReadFile(mainFile)
if err != nil {
printer.Warningf("Error while preparing main file: %#v", err)
}
output := strings.Replace(string(input), "github.com/iris-contrib/iris-command-assets/"+packageName+"/", targetDir+"/", -1)
err = ioutil.WriteFile(mainFile, []byte(output), 0644)
if err != nil {
printer.Warningf("Error while preparing main file: %#v", err)
}
printer.Infof("%s package was installed successfully [%s]", packageName, installTo)
// build & run the server
// go build
buildCmd := utils.CommandBuilder("go", "build")
if installTo[len(installTo)-1] != os.PathSeparator || installTo[len(installTo)-1] != '/' {
installTo += utils.PathSeparator
}
buildCmd.Dir = installTo + "backend"
buildCmd.Stderr = os.Stderr
err = buildCmd.Start()
if err != nil {
printer.Warningf("\n Failed to build the %s package. Trace: %s", packageName, err.Error())
}
buildCmd.Wait()
print("\n\n")
// run backend/backend(.exe)
executable := "backend"
if runtime.GOOS == "windows" {
executable += ".exe"
}
runCmd := utils.CommandBuilder("." + utils.PathSeparator + executable)
runCmd.Dir = buildCmd.Dir
runCmd.Stdout = os.Stdout
runCmd.Stderr = os.Stderr
err = runCmd.Start()
if err != nil {
printer.Warningf("\n Failed to run the %s package. Trace: %s", packageName, err.Error())
}
runCmd.Wait()
return err
}

View File

@ -5,8 +5,14 @@ package main
go get -u github.com/kataras/iris/iris go get -u github.com/kataras/iris/iris
// create command
create an empty folder, open the command prompt/terminal there, type and press enter: create an empty folder, open the command prompt/terminal there, type and press enter:
iris create iris create
// run command
navigate to your app's directory and execute:
iris run main.go
*/ */

View File

@ -3,155 +3,61 @@ package main
import ( import (
"os" "os"
_ "syscall"
"strings" "strings"
"io/ioutil"
"runtime"
"github.com/fatih/color"
"github.com/kataras/cli" "github.com/kataras/cli"
"github.com/kataras/iris" "github.com/kataras/iris"
"github.com/kataras/iris/utils" "github.com/kataras/iris/config"
) "github.com/kataras/iris/logger"
const (
// PackagesURL the url to download all the packages
PackagesURL = "https://github.com/iris-contrib/iris-command-assets/archive/master.zip"
// PackagesExportedName the folder created after unzip
PackagesExportedName = "iris-command-assets-master"
) )
var ( var (
app *cli.App app *cli.App
// SuccessPrint prints with a green color printer *logger.Logger
SuccessPrint = color.New(color.FgGreen).Add(color.Bold).PrintfFunc() workingDir string
// InfoPrint prints with the cyan color
InfoPrint = color.New(color.FgHiCyan).Add(color.Bold).PrintfFunc()
packagesInstallDir = os.Getenv("GOPATH") + utils.PathSeparator + "src" + utils.PathSeparator + "github.com" + utils.PathSeparator + "iris-contrib" + utils.PathSeparator + "iris-command-assets" + utils.PathSeparator
) )
func init() { func init() {
app = cli.NewApp("iris", "Command line tool for Iris web framework", "0.0.4")
// set the current working dir
if d, err := os.Getwd(); err != nil {
panic(err)
} else {
workingDir = d
}
// defaultInstallDir is the default directory which the create will copy and run the package when finish downloading
// it's just the last path part of the workingDir
defaultInstallDir := workingDir[strings.LastIndexByte(workingDir, os.PathSeparator)+1:]
// init the cli app
app = cli.NewApp("iris", "Command line tool for Iris web framework", "0.0.5")
// version command
app.Command(cli.Command("version", "\t prints your iris version").Action(func(cli.Flags) error { app.Printf("%s", iris.Version); return nil })) app.Command(cli.Command("version", "\t prints your iris version").Action(func(cli.Flags) error { app.Printf("%s", iris.Version); return nil }))
// create command/-/create.go
createCmd := cli.Command("create", "create a project to a given directory"). createCmd := cli.Command("create", "create a project to a given directory").
Flag("offline", false, "set to true to disable the packages download on each create command"). Flag("offline", false, "set to true to disable the packages download on each create command").
Flag("dir", "myiris", "$GOPATH/src/$dir the directory to install the sample package"). Flag("dir", defaultInstallDir, "$GOPATH/src/$dir the directory to install the sample package").
Flag("type", "basic", "creates a project based on the -t package. Currently, available types are 'basic' & 'static'"). Flag("type", "basic", "creates a project based on the -t package. Currently, available types are 'basic' & 'static'").
Action(create) Action(create)
// run command/-/run.go
runAndWatchCmd := cli.Command("run", "runs and reload on source code changes, example: iris run main.go").Action(runAndWatch)
// register the commands
app.Command(createCmd) app.Command(createCmd)
app.Command(runAndWatchCmd)
// init the logger
printer = logger.New(config.DefaultLogger())
} }
func main() { func main() {
app.Run(func(cli.Flags) error { return nil }) // run the application
} app.Run(func(f cli.Flags) error {
return nil
func create(flags cli.Flags) (err error) { })
if !utils.DirectoryExists(packagesInstallDir) || !flags.Bool("offline") {
downloadPackages()
}
targetDir := flags.String("dir")
// remove first and last / if any
if strings.HasPrefix(targetDir, "./") || strings.HasPrefix(targetDir, "."+utils.PathSeparator) {
targetDir = targetDir[2:]
}
if targetDir[len(targetDir)-1] == '/' {
targetDir = targetDir[0 : len(targetDir)-1]
}
//
createPackage(flags.String("type"), targetDir)
return
}
func downloadPackages() {
errMsg := "\nProblem while downloading the assets from the internet for the first time. Trace: %s"
installedDir, err := utils.Install(PackagesURL, packagesInstallDir)
if err != nil {
app.Printf(errMsg, err.Error())
return
}
// installedDir is the packagesInstallDir+PackagesExportedName, we will copy these contents to the parent, to the packagesInstallDir, because of import paths.
err = utils.CopyDir(installedDir, packagesInstallDir)
if err != nil {
app.Printf(errMsg, err.Error())
return
}
// we don't exit on errors here.
// try to remove the unzipped folder
utils.RemoveFile(installedDir[0 : len(installedDir)-1])
}
func createPackage(packageName string, targetDir string) error {
installTo := os.Getenv("GOPATH") + utils.PathSeparator + "src" + utils.PathSeparator + targetDir
packageDir := packagesInstallDir + utils.PathSeparator + packageName
err := utils.CopyDir(packageDir, installTo)
if err != nil {
app.Printf("\nProblem while copying the %s package to the %s. Trace: %s", packageName, installTo, err.Error())
return err
}
// now replace main.go's 'github.com/iris-contrib/iris-command-assets/basic/' with targetDir
// hardcode all that, we don't have anything special and neither will do
targetDir = strings.Replace(targetDir, "\\", "/", -1) // for any case
mainFile := installTo + utils.PathSeparator + "backend" + utils.PathSeparator + "main.go"
input, err := ioutil.ReadFile(mainFile)
if err != nil {
app.Printf("Error while preparing main file: %#v", err)
}
output := strings.Replace(string(input), "github.com/iris-contrib/iris-command-assets/"+packageName+"/", targetDir+"/", -1)
err = ioutil.WriteFile(mainFile, []byte(output), 0644)
if err != nil {
app.Printf("Error while preparing main file: %#v", err)
}
InfoPrint("%s package was installed successfully", packageName)
// build & run the server
// go build
buildCmd := utils.CommandBuilder("go", "build")
if installTo[len(installTo)-1] != os.PathSeparator || installTo[len(installTo)-1] != '/' {
installTo += utils.PathSeparator
}
buildCmd.Dir = installTo + "backend"
buildCmd.Stderr = os.Stderr
err = buildCmd.Start()
if err != nil {
app.Printf("\n Failed to build the %s package. Trace: %s", packageName, err.Error())
}
buildCmd.Wait()
print("\n\n")
// run backend/backend(.exe)
executable := "backend"
if runtime.GOOS == "windows" {
executable += ".exe"
}
runCmd := utils.CommandBuilder("." + utils.PathSeparator + executable)
runCmd.Dir = buildCmd.Dir
runCmd.Stdout = os.Stdout
runCmd.Stderr = os.Stderr
err = runCmd.Start()
if err != nil {
app.Printf("\n Failed to run the %s package. Trace: %s", packageName, err.Error())
}
runCmd.Wait()
return err
} }

96
iris/run.go Normal file
View File

@ -0,0 +1,96 @@
package main
import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync/atomic"
"github.com/kataras/cli"
"github.com/kataras/iris/errors"
"github.com/kataras/iris/utils"
)
var (
errInvalidArgs = errors.New("Invalid arguments [%s], type -h to get assistant")
errInvalidExt = errors.New("%s is not a go program")
errUnexpected = errors.New("Unexpected error!!! Please post an issue here: https://github.com/kataras/iris/issues")
goExt = ".go"
)
func runAndWatch(flags cli.Flags) error {
if len(os.Args) <= 2 {
err := errInvalidArgs.Format(strings.Join(os.Args, ","))
printer.Dangerf(err.Error())
return err
}
programPath := ""
filenameCh := make(chan string)
if len(os.Args) > 2 { // iris run main.go
programPath = os.Args[2]
if programPath[len(programPath)-1] == '/' {
programPath = programPath[0 : len(programPath)-1]
}
if filepath.Ext(programPath) != goExt {
return errInvalidExt.Format(programPath)
}
}
// here(below), we don't return the error because the -help command doesn't help the user for these errors.
// run the file watcher before all, because the user maybe has a go syntax error before the first run
utils.WatchDirectoryChanges(workingDir, func(fname string) {
if filepath.Ext(fname) == goExt {
filenameCh <- fname
}
}, printer)
// we don't use go build and run from the executable, for performance reasons, no need this is a development action already
goRun := utils.CommandBuilder("go", "run", programPath)
goRun.Dir = workingDir
goRun.Stdout = os.Stdout
goRun.Stderr = os.Stderr
if err := goRun.Start(); err != nil {
printer.Dangerf("\n [ERROR] Failed to run the %s iris program. Trace: %s", programPath, err.Error())
return nil
}
isWindows := runtime.GOOS == "windows"
defer func() {
printer.Dangerf("")
printer.Panic(errUnexpected)
}()
var times uint32 = 1
for {
select {
case fname := <-filenameCh:
{
// it's not a warning but I like to use purple color for this message
printer.Warningf("\n/-%d-/ File '%s' changed, re-running...", atomic.LoadUint32(&times), fname)
// force kill, sometimes runCmd.Process.Kill or Signal(os.Kill) doesn't kill the child of the go's go run command ( which is the iris program)
if isWindows {
utils.CommandBuilder("taskkill", "/F", "/T", "/PID", strconv.Itoa(goRun.Process.Pid)).Run()
} else {
utils.CommandBuilder("kill", "-INT", "-"+strconv.Itoa(goRun.Process.Pid)).Run()
}
goRun = utils.CommandBuilder("go", "run", programPath)
goRun.Dir = workingDir
goRun.Stderr = os.Stderr
if err := goRun.Start(); err != nil {
printer.Warningf("\n [ERROR ON RELOAD] Failed to run the %s iris program. Trace: %s", programPath, err.Error())
} else {
atomic.AddUint32(&times, 1)
// don't print success on anything here because we may have error on iris itself, no need to print any message we are no spammers.
}
}
}
}
}

View File

@ -2,7 +2,6 @@ package iriscontrol
import ( import (
"os" "os"
"runtime"
"github.com/kataras/iris" "github.com/kataras/iris"
"github.com/kataras/iris/utils" "github.com/kataras/iris/utils"
@ -20,15 +19,8 @@ var (
// init just sets the assetsPath & current workingDir // init just sets the assetsPath & current workingDir
func init() { func init() {
homepath := ""
if runtime.GOOS == "windows" {
homepath = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
} else {
homepath = os.Getenv("HOME")
}
assetsPath = homepath + utils.PathSeparator + ".iris" + utils.PathSeparator + "iris-control-assets" + utils.PathSeparator
workingDir, _ = os.Getwd() workingDir, _ = os.Getwd()
assetsPath = utils.AssetsDirectory + utils.PathSeparator + "iris-control-assets" + utils.PathSeparator
} }
func installAssets() { func installAssets() {

View File

@ -9,8 +9,12 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"time" "time"
"github.com/fsnotify/fsnotify"
"github.com/kataras/iris/logger"
) )
const ( const (
@ -18,6 +22,23 @@ const (
ContentBINARY = "application/octet-stream" ContentBINARY = "application/octet-stream"
) )
var (
// AssetsDirectory the path which iris saves some assets came from the internet ( used in iris control plugin (to download the html,css,js) and for iris command line tool to download the packages)
AssetsDirectory = ""
)
// init just sets the iris path for assets, used in iris control plugin and for iris command line tool(create command)
// the AssetsDirectory path should be like: C:/users/kataras/.iris (for windows) and for linux you can imagine
func init() {
homepath := ""
if runtime.GOOS == "windows" {
homepath = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
} else {
homepath = os.Getenv("HOME")
}
AssetsDirectory = homepath + PathSeparator + ".iris"
}
// DirectoryExists returns true if a directory(or file) exists, otherwise false // DirectoryExists returns true if a directory(or file) exists, otherwise false
func DirectoryExists(dir string) bool { func DirectoryExists(dir string) bool {
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
@ -316,21 +337,14 @@ func GetParentDir(targetDirectory string) string {
// 3-BSD License for package fsnotify/fsnotify // 3-BSD License for package fsnotify/fsnotify
// Copyright (c) 2012 The Go Authors. All rights reserved. // Copyright (c) 2012 The Go Authors. All rights reserved.
// Copyright (c) 2012 fsnotify Authors. All rights reserved. // Copyright (c) 2012 fsnotify Authors. All rights reserved.
"github.com/fsnotify/fsnotify" */
//
"github.com/kataras/iris/errors"
"github.com/kataras/iris/logger"
// WatchDirectoryChanges watches for directory changes and calls the 'evt' callback parameter // WatchDirectoryChanges watches a directory and fires the callback with the changed name, receives a logger just to print with red letters any errors, no need for second callback.
// unused after v2 but propably I will bring it back on v3 func WatchDirectoryChanges(rootPath string, evt func(filename string), logger *logger.Logger) {
isWindows := runtime.GOOS == "windows"
func WatchDirectoryChanges(rootPath string, evt func(filename string), logger ...*logger.Logger) { watcher, werr := fsnotify.NewWatcher()
watcher, err := fsnotify.NewWatcher() if werr != nil {
logger.Dangerf(werr.Error())
if err != nil {
if len(logger) > 0 {
errors.Printf(logger[0], err)
}
return return
} }
@ -343,7 +357,7 @@ func WatchDirectoryChanges(rootPath string, evt func(filename string), logger ..
if event.Op&fsnotify.Write == fsnotify.Write { if event.Op&fsnotify.Write == fsnotify.Write {
//this is received two times, the last time is the real changed file, so //this is received two times, the last time is the real changed file, so
i++ i++
if i%2 == 0 { if i%2 == 0 || !isWindows { // this 'hack' works for windows but I dont know if works for linux too, we can wait for issue reports here.
if time.Now().After(lastChange.Add(time.Duration(1) * time.Second)) { if time.Now().After(lastChange.Add(time.Duration(1) * time.Second)) {
lastChange = time.Now() lastChange = time.Now()
evt(event.Name) evt(event.Name)
@ -352,18 +366,15 @@ func WatchDirectoryChanges(rootPath string, evt func(filename string), logger ..
} }
case err := <-watcher.Errors: case err := <-watcher.Errors:
if len(logger) > 0 { logger.Dangerf(err.Error())
errors.Printf(logger[0], err)
}
} }
} }
}() }()
err = watcher.Add(rootPath) werr = watcher.Add(rootPath)
if err != nil { if werr != nil {
if len(logger) > 0 { logger.Dangerf(werr.Error())
errors.Printf(logger[0], err)
}
} }
}*/ }