From fef9ab4f0634c5f0a72148e0e96b73f7de63e969 Mon Sep 17 00:00:00 2001 From: Arthur Barr Date: Wed, 22 Nov 2017 16:13:30 +0000 Subject: [PATCH] Fix runmqserver error handling --- cmd/runmqserver/license.go | 91 ++++++++++++ cmd/runmqserver/main.go | 216 ++++++++++------------------ cmd/runmqserver/signals.go | 79 ++++++++++ test/docker/docker_api_test_util.go | 8 +- 4 files changed, 252 insertions(+), 142 deletions(-) create mode 100644 cmd/runmqserver/license.go create mode 100644 cmd/runmqserver/signals.go diff --git a/cmd/runmqserver/license.go b/cmd/runmqserver/license.go new file mode 100644 index 0000000..c275a9f --- /dev/null +++ b/cmd/runmqserver/license.go @@ -0,0 +1,91 @@ +/* +© Copyright IBM Corporation 2017 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "errors" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" +) + +// resolveLicenseFile returns the file name of the MQ license file, taking into +// account the language set by the LANG environment variable +func resolveLicenseFile() string { + lang, ok := os.LookupEnv("LANG") + if !ok { + return "English.txt" + } + switch { + case strings.HasPrefix(lang, "zh_TW"): + return "Chinese_TW.txt" + case strings.HasPrefix(lang, "zh"): + return "Chinese.txt" + case strings.HasPrefix(lang, "cs"): + return "Czech.txt" + case strings.HasPrefix(lang, "fr"): + return "French.txt" + case strings.HasPrefix(lang, "de"): + return "German.txt" + case strings.HasPrefix(lang, "el"): + return "Greek.txt" + case strings.HasPrefix(lang, "id"): + return "Indonesian.txt" + case strings.HasPrefix(lang, "it"): + return "Italian.txt" + case strings.HasPrefix(lang, "ja"): + return "Japanese.txt" + case strings.HasPrefix(lang, "ko"): + return "Korean.txt" + case strings.HasPrefix(lang, "lt"): + return "Lithuanian.txt" + case strings.HasPrefix(lang, "pl"): + return "Polish.txt" + case strings.HasPrefix(lang, "pt"): + return "Portugese.txt" + case strings.HasPrefix(lang, "ru"): + return "Russian.txt" + case strings.HasPrefix(lang, "sl"): + return "Slovenian.txt" + case strings.HasPrefix(lang, "es"): + return "Spanish.txt" + case strings.HasPrefix(lang, "tr"): + return "Turkish.txt" + } + return "English.txt" +} + +func checkLicense() (bool, error) { + lic, ok := os.LookupEnv("LICENSE") + switch { + case ok && lic == "accept": + return true, nil + case ok && lic == "view": + file := filepath.Join("/opt/mqm/licenses", resolveLicenseFile()) + buf, err := ioutil.ReadFile(file) + if err != nil { + log.Println(err) + return false, err + } + log.Println(string(buf)) + return false, nil + } + log.Println("Error: Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions.") + log.Println("License agreements and information can be viewed by setting the environment variable LICENSE=view. You can also set the LANG environment variable to view the license in a different language.") + return false, errors.New("Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions") +} diff --git a/cmd/runmqserver/main.go b/cmd/runmqserver/main.go index 3e47ef9..173fbbc 100644 --- a/cmd/runmqserver/main.go +++ b/cmd/runmqserver/main.go @@ -18,135 +18,73 @@ limitations under the License. package main import ( - "fmt" + "errors" "io/ioutil" "log" "os" "os/exec" - "os/signal" "path/filepath" "strings" - "syscall" "github.com/ibm-messaging/mq-container/internal/command" "github.com/ibm-messaging/mq-container/internal/name" - "golang.org/x/sys/unix" ) -// resolveLicenseFile returns the file name of the MQ license file, taking into -// account the language set by the LANG environment variable -func resolveLicenseFile() string { - lang, ok := os.LookupEnv("LANG") - if !ok { - return "English.txt" - } - switch { - case strings.HasPrefix(lang, "zh_TW"): - return "Chinese_TW.txt" - case strings.HasPrefix(lang, "zh"): - return "Chinese.txt" - case strings.HasPrefix(lang, "cs"): - return "Czech.txt" - case strings.HasPrefix(lang, "fr"): - return "French.txt" - case strings.HasPrefix(lang, "de"): - return "German.txt" - case strings.HasPrefix(lang, "el"): - return "Greek.txt" - case strings.HasPrefix(lang, "id"): - return "Indonesian.txt" - case strings.HasPrefix(lang, "it"): - return "Italian.txt" - case strings.HasPrefix(lang, "ja"): - return "Japanese.txt" - case strings.HasPrefix(lang, "ko"): - return "Korean.txt" - case strings.HasPrefix(lang, "lt"): - return "Lithuanian.txt" - case strings.HasPrefix(lang, "pl"): - return "Polish.txt" - case strings.HasPrefix(lang, "pt"): - return "Portugese.txt" - case strings.HasPrefix(lang, "ru"): - return "Russian.txt" - case strings.HasPrefix(lang, "sl"): - return "Slovenian.txt" - case strings.HasPrefix(lang, "es"): - return "Spanish.txt" - case strings.HasPrefix(lang, "tr"): - return "Turkish.txt" - } - return "English.txt" -} - -func checkLicense() { - lic, ok := os.LookupEnv("LICENSE") - switch { - case ok && lic == "accept": - return - case ok && lic == "view": - file := filepath.Join("/opt/mqm/licenses", resolveLicenseFile()) - buf, err := ioutil.ReadFile(file) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - fmt.Println(string(buf)) - os.Exit(1) - } - fmt.Println("Error: Set environment variable LICENSE=accept to indicate acceptance of license terms and conditions.") - fmt.Println("License agreements and information can be viewed by setting the environment variable LICENSE=view. You can also set the LANG environment variable to view the license in a different language.") - os.Exit(1) -} - // createDirStructure creates the default MQ directory structure under /var/mqm -func createDirStructure() { +func createDirStructure() error { out, _, err := command.Run("/opt/mqm/bin/crtmqdir", "-f", "-s") if err != nil { - log.Fatalf("Error creating directory structure: %v\n", string(out)) + log.Printf("Error creating directory structure: %v\n", string(out)) + return err } log.Println("Created directory structure under /var/mqm") + return nil } -func createQueueManager(name string) { +func createQueueManager(name string) error { log.Printf("Creating queue manager %v", name) out, rc, err := command.Run("crtmqm", "-q", "-p", "1414", name) if err != nil { // 8=Queue manager exists, which is fine if rc != 8 { log.Printf("crtmqm returned %v", rc) - log.Fatalln(string(out)) - } else { - log.Printf("Detected existing queue manager %v", name) - return + log.Println(string(out)) + return err } + log.Printf("Detected existing queue manager %v", name) } + return nil } -func updateCommandLevel() { +func updateCommandLevel() error { level, ok := os.LookupEnv("MQ_CMDLEVEL") if ok && level != "" { out, rc, err := command.Run("strmqm", "-e", "CMDLEVEL="+level) if err != nil { - log.Fatalf("Error %v setting CMDLEVEL: %v", rc, string(out)) + log.Printf("Error %v setting CMDLEVEL: %v", rc, string(out)) + return err } } + return nil } -func startQueueManager() { +func startQueueManager() error { log.Println("Starting queue manager") out, rc, err := command.Run("strmqm") if err != nil { - log.Fatalf("Error %v starting queue manager: %v", rc, string(out)) + log.Printf("Error %v starting queue manager: %v", rc, string(out)) + return err } log.Println("Started queue manager") + return nil } -func configureQueueManager() { +func configureQueueManager() error { const configDir string = "/etc/mqm" files, err := ioutil.ReadDir(configDir) if err != nil { - log.Fatal(err) + log.Println(err) + return err } for _, file := range files { @@ -154,12 +92,14 @@ func configureQueueManager() { abs := filepath.Join(configDir, file.Name()) mqsc, err := ioutil.ReadFile(abs) if err != nil { - log.Fatal(err) + log.Println(err) + return err } cmd := exec.Command("runmqsc") stdin, err := cmd.StdinPipe() if err != nil { - log.Fatal(err) + log.Println(err) + return err } stdin.Write(mqsc) stdin.Close() @@ -172,78 +112,76 @@ func configureQueueManager() { log.Printf("Output for \"runmqsc\" with %v:\n\t%v", abs, strings.Replace(string(out), "\n", "\n\t", -1)) } } + return nil } -func stopQueueManager() { +func stopQueueManager(name string) error { log.Println("Stopping queue manager") - out, _, err := command.Run("endmqm", "-w") + out, _, err := command.Run("endmqm", "-w", name) if err != nil { - log.Fatalf("Error stopping queue manager: %v", string(out)) + log.Printf("Error stopping queue manager: %v", string(out)) + return err } log.Println("Stopped queue manager") + return nil } -// createTerminateChannel creates a channel which will be closed when SIGTERM -// is received. -func createTerminateChannel() chan struct{} { - done := make(chan struct{}) - // Handle SIGTERM - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) - go func() { - sig := <-c - log.Printf("Signal received: %v", sig) - stopQueueManager() - close(done) - }() - return done -} - -// createReaperChannel creates a channel which will be used to reap zombie -// (defunct) processes. This is a responsibility of processes running -// as PID 1. -func createReaper() { - // Handle SIGCHLD - c := make(chan os.Signal, 3) - signal.Notify(c, syscall.SIGCHLD) - go func() { - for { - <-c - for { - var ws unix.WaitStatus - _, err := unix.Wait4(-1, &ws, 0, nil) - // If err indicates "no child processes" left to reap, then - // wait for next SIGCHLD signal - if err == unix.ECHILD { - break - } - } - } - }() -} - -func main() { - createReaper() - checkLicense() - // Start SIGTERM handler channel - done := createTerminateChannel() +func doMain() error { + accepted, err := checkLicense() + if err != nil { + return err + } + if !accepted { + return errors.New("License not accepted") + } name, err := name.GetQueueManagerName() if err != nil { - log.Fatalln(err) + log.Println(err) + return err } log.Printf("Using queue manager name: %v", name) + // Start signal handler + signalControl := signalHandler(name) + logConfig() err = createVolume("/mnt/mqm") if err != nil { - log.Fatal(err) + log.Println(err) + return err + } + err = createDirStructure() + if err != nil { + return err + } + err = createQueueManager(name) + if err != nil { + return err + } + err = updateCommandLevel() + if err != nil { + return err + } + err = startQueueManager() + if err != nil { + return err } - createDirStructure() - createQueueManager(name) - updateCommandLevel() - startQueueManager() configureQueueManager() + // Start reaping zombies from now on. + // Start this here, so that we don't reap any sub-processes created + // by this process (e.g. for crtmqm or strmqm) + signalControl <- startReaping + // Reap zombies now, just in case we've already got some + signalControl <- reapNow // Wait for terminate signal - <-done + <-signalControl + return nil +} + +func main() { + err := doMain() + if err != nil { + os.Exit(1) + } } diff --git a/cmd/runmqserver/signals.go b/cmd/runmqserver/signals.go new file mode 100644 index 0000000..04c686d --- /dev/null +++ b/cmd/runmqserver/signals.go @@ -0,0 +1,79 @@ +/* +© Copyright IBM Corporation 2017 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import ( + "log" + "os" + "os/signal" + "syscall" + + "golang.org/x/sys/unix" +) + +const ( + startReaping = iota + reapNow = iota +) + +func signalHandler(qmgr string) chan int { + control := make(chan int) + // Use separate channels for the signals, to avoid SIGCHLD signals swamping + // the buffer, and preventing other signals. + stopSignals := make(chan os.Signal) + reapSignals := make(chan os.Signal) + signal.Notify(stopSignals, syscall.SIGTERM, syscall.SIGINT) + go func() { + for { + select { + case sig := <-stopSignals: + log.Printf("Signal received: %v", sig) + signal.Stop(reapSignals) + signal.Stop(stopSignals) + stopQueueManager(qmgr) + // One final reap + reapZombies() + close(control) + // End the goroutine + return + case <-reapSignals: + reapZombies() + case job := <-control: + switch { + case job == startReaping: + // Add SIGCHLD to the list of signals we're listening to + signal.Notify(reapSignals, syscall.SIGCHLD) + case job == reapNow: + reapZombies() + } + } + } + }() + return control +} + +// reapZombies reaps any zombie (terminated) processes now. +// This function should be called before exiting. +func reapZombies() { + for { + var ws unix.WaitStatus + pid, err := unix.Wait4(-1, &ws, unix.WNOHANG, nil) + // If err or pid indicate "no child processes" + if pid == 0 || err == unix.ECHILD { + return + } + } +} diff --git a/test/docker/docker_api_test_util.go b/test/docker/docker_api_test_util.go index d67111d..5af6716 100644 --- a/test/docker/docker_api_test_util.go +++ b/test/docker/docker_api_test_util.go @@ -61,13 +61,15 @@ func cleanContainer(t *testing.T, cli *client.Client, ID string) { // Log the results and continue t.Logf("Inspected container %v: %#v", ID, i) } - t.Logf("Killing container: %v", ID) - // Kill the container. This allows the coverage output to be generated. - err = cli.ContainerKill(context.Background(), ID, "SIGTERM") + t.Logf("Stopping container: %v", ID) + timeout := 10 * time.Second + // Stop the container. This allows the coverage output to be generated. + err = cli.ContainerStop(context.Background(), ID, &timeout) if err != nil { // Just log the error and continue t.Log(err) } + t.Log("Container stopped") // If a code coverage file has been generated, then rename it to match the test name os.Rename(filepath.Join(coverageDir(t), "container.cov"), filepath.Join(coverageDir(t), t.Name()+".cov")) // Log the container output for any container we're about to delete