vitess.io/vitess@v0.16.2/go/vt/mysqlctl/mysqld.go (about) 1 /* 2 Copyright 2019 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 /* 18 Commands for controlling an external mysql process. 19 20 Some commands are issued as exec'd tools, some are handled by connecting via 21 the mysql protocol. 22 */ 23 24 package mysqlctl 25 26 import ( 27 "bufio" 28 "bytes" 29 "context" 30 "errors" 31 "fmt" 32 "io" 33 "os" 34 "os/exec" 35 "path" 36 "path/filepath" 37 "regexp" 38 "strconv" 39 "strings" 40 "sync" 41 "time" 42 43 "github.com/spf13/pflag" 44 45 "vitess.io/vitess/config" 46 "vitess.io/vitess/go/mysql" 47 "vitess.io/vitess/go/vt/dbconfigs" 48 "vitess.io/vitess/go/vt/dbconnpool" 49 "vitess.io/vitess/go/vt/hook" 50 "vitess.io/vitess/go/vt/log" 51 "vitess.io/vitess/go/vt/mysqlctl/mysqlctlclient" 52 "vitess.io/vitess/go/vt/servenv" 53 54 vtenv "vitess.io/vitess/go/vt/env" 55 ) 56 57 var ( 58 59 // DisableActiveReparents is a flag to disable active 60 // reparents for safety reasons. It is used in three places: 61 // 1. in this file to skip registering the commands. 62 // 2. in vtctld so it can be exported to the UI (different 63 // package, that's why it's exported). That way we can disable 64 // menu items there, using features. 65 DisableActiveReparents bool 66 67 dbaPoolSize = 20 68 // DbaIdleTimeout is how often we will refresh the DBA connpool connections 69 DbaIdleTimeout = time.Minute 70 appPoolSize = 40 71 appIdleTimeout = time.Minute 72 73 // PoolDynamicHostnameResolution is whether we should retry DNS resolution of hostname targets 74 // and reconnect if necessary 75 PoolDynamicHostnameResolution time.Duration 76 77 mycnfTemplateFile string 78 socketFile string 79 80 replicationConnectRetry = 10 * time.Second 81 82 versionRegex = regexp.MustCompile(`Ver ([0-9]+)\.([0-9]+)\.([0-9]+)`) 83 ) 84 85 // How many bytes from MySQL error log to sample for error messages 86 const maxLogFileSampleSize = 4096 87 88 // Mysqld is the object that represents a mysqld daemon running on this server. 89 type Mysqld struct { 90 dbcfgs *dbconfigs.DBConfigs 91 dbaPool *dbconnpool.ConnectionPool 92 appPool *dbconnpool.ConnectionPool 93 94 capabilities capabilitySet 95 96 // mutex protects the fields below. 97 mutex sync.Mutex 98 onTermFuncs []func() 99 cancelWaitCmd chan struct{} 100 } 101 102 func init() { 103 for _, cmd := range []string{"mysqlctl", "mysqlctld", "vtcombo", "vttablet", "vttestserver"} { 104 servenv.OnParseFor(cmd, registerMySQLDFlags) 105 } 106 for _, cmd := range []string{"vtcombo", "vttablet", "vttestserver", "vtctld", "vtctldclient"} { 107 servenv.OnParseFor(cmd, registerReparentFlags) 108 } 109 for _, cmd := range []string{"mysqlctl", "mysqlctld", "vtcombo", "vttablet", "vttestserver"} { 110 servenv.OnParseFor(cmd, registerPoolFlags) 111 } 112 } 113 114 func registerMySQLDFlags(fs *pflag.FlagSet) { 115 fs.DurationVar(&PoolDynamicHostnameResolution, "pool_hostname_resolve_interval", PoolDynamicHostnameResolution, "if set force an update to all hostnames and reconnect if changed, defaults to 0 (disabled)") 116 fs.StringVar(&mycnfTemplateFile, "mysqlctl_mycnf_template", mycnfTemplateFile, "template file to use for generating the my.cnf file during server init") 117 fs.StringVar(&socketFile, "mysqlctl_socket", socketFile, "socket file to use for remote mysqlctl actions (empty for local actions)") 118 fs.DurationVar(&replicationConnectRetry, "replication_connect_retry", replicationConnectRetry, "how long to wait in between replica reconnect attempts. Only precise to the second.") 119 } 120 121 func registerReparentFlags(fs *pflag.FlagSet) { 122 fs.BoolVar(&DisableActiveReparents, "disable_active_reparents", DisableActiveReparents, "if set, do not allow active reparents. Use this to protect a cluster using external reparents.") 123 } 124 125 func registerPoolFlags(fs *pflag.FlagSet) { 126 fs.IntVar(&dbaPoolSize, "dba_pool_size", dbaPoolSize, "Size of the connection pool for dba connections") 127 fs.DurationVar(&DbaIdleTimeout, "dba_idle_timeout", DbaIdleTimeout, "Idle timeout for dba connections") 128 fs.DurationVar(&appIdleTimeout, "app_idle_timeout", appIdleTimeout, "Idle timeout for app connections") 129 fs.IntVar(&appPoolSize, "app_pool_size", appPoolSize, "Size of the connection pool for app connections") 130 } 131 132 // NewMysqld creates a Mysqld object based on the provided configuration 133 // and connection parameters. 134 func NewMysqld(dbcfgs *dbconfigs.DBConfigs) *Mysqld { 135 result := &Mysqld{ 136 dbcfgs: dbcfgs, 137 } 138 139 // Create and open the connection pool for dba access. 140 result.dbaPool = dbconnpool.NewConnectionPool("DbaConnPool", dbaPoolSize, DbaIdleTimeout, 0, PoolDynamicHostnameResolution) 141 result.dbaPool.Open(dbcfgs.DbaWithDB()) 142 143 // Create and open the connection pool for app access. 144 result.appPool = dbconnpool.NewConnectionPool("AppConnPool", appPoolSize, appIdleTimeout, 0, PoolDynamicHostnameResolution) 145 result.appPool.Open(dbcfgs.AppWithDB()) 146 147 /* 148 Unmanaged tablets are special because the MYSQL_FLAVOR detection 149 will not be accurate because the mysqld might not be the same 150 one as the server started. 151 152 This skips the panic that checks that we can detect a server, 153 but also relies on none of the flavor detection features being 154 used at runtime. Currently this assumption is guaranteed true. 155 */ 156 if dbconfigs.GlobalDBConfigs.HasGlobalSettings() { 157 log.Info("mysqld is unmanaged or remote. Skipping flavor detection") 158 return result 159 } 160 version, getErr := GetVersionString() 161 f, v, err := ParseVersionString(version) 162 163 /* 164 By default Vitess searches in vtenv.VtMysqlRoot() for a mysqld binary. 165 This is historically the VT_MYSQL_ROOT env, but if it is unset or empty, 166 Vitess will search the PATH. See go/vt/env/env.go. 167 168 A number of subdirs inside vtenv.VtMysqlRoot() will be searched, see 169 func binaryPath() for context. If no mysqld binary is found (possibly 170 because it is in a container or both VT_MYSQL_ROOT and VTROOT are set 171 incorrectly), there will be a fallback to using the MYSQL_FLAVOR env 172 variable. 173 174 If MYSQL_FLAVOR is not defined, there will be a panic. 175 176 Note: relying on MySQL_FLAVOR is not recommended, since for historical 177 purposes "MySQL56" actually means MySQL 5.7, which is a very strange 178 behavior. 179 */ 180 181 if getErr != nil || err != nil { 182 f, v, err = GetVersionFromEnv() 183 if err != nil { 184 vtenvMysqlRoot, _ := vtenv.VtMysqlRoot() 185 message := fmt.Sprintf(`could not auto-detect MySQL version. You may need to set your PATH so a mysqld binary can be found, or set the environment variable MYSQL_FLAVOR if mysqld is not available locally: 186 PATH: %s 187 VT_MYSQL_ROOT: %s 188 VTROOT: %s 189 vtenv.VtMysqlRoot(): %s 190 MYSQL_FLAVOR: %s 191 `, 192 os.Getenv("PATH"), 193 os.Getenv("VT_MYSQL_ROOT"), 194 os.Getenv("VTROOT"), 195 vtenvMysqlRoot, 196 os.Getenv("MYSQL_FLAVOR")) 197 panic(message) 198 } 199 } 200 201 log.Infof("Using flavor: %v, version: %v", f, v) 202 result.capabilities = newCapabilitySet(f, v) 203 return result 204 } 205 206 /* 207 GetVersionFromEnv returns the flavor and an assumed version based on the legacy 208 MYSQL_FLAVOR environment variable. 209 210 The assumed version may not be accurate since the legacy variable only specifies 211 broad families of compatible versions. However, the differences between those 212 versions should only matter if Vitess is managing the lifecycle of mysqld, in which 213 case we should have a local copy of the mysqld binary from which we can fetch 214 the accurate version instead of falling back to this function (see GetVersionString). 215 */ 216 func GetVersionFromEnv() (flavor MySQLFlavor, ver ServerVersion, err error) { 217 env := os.Getenv("MYSQL_FLAVOR") 218 switch env { 219 case "MariaDB": 220 return FlavorMariaDB, ServerVersion{10, 6, 11}, nil 221 case "MySQL80": 222 return FlavorMySQL, ServerVersion{8, 0, 11}, nil 223 case "MySQL56": 224 return FlavorMySQL, ServerVersion{5, 7, 10}, nil 225 } 226 return flavor, ver, fmt.Errorf("could not determine version from MYSQL_FLAVOR: %s", env) 227 } 228 229 // GetVersionString runs mysqld --version and returns its output as a string 230 func GetVersionString() (string, error) { 231 mysqlRoot, err := vtenv.VtMysqlRoot() 232 if err != nil { 233 return "", err 234 } 235 mysqldPath, err := binaryPath(mysqlRoot, "mysqld") 236 if err != nil { 237 return "", err 238 } 239 _, version, err := execCmd(mysqldPath, []string{"--version"}, nil, mysqlRoot, nil) 240 if err != nil { 241 return "", err 242 } 243 return version, nil 244 } 245 246 // ParseVersionString parses the output of mysqld --version into a flavor and version 247 func ParseVersionString(version string) (flavor MySQLFlavor, ver ServerVersion, err error) { 248 if strings.Contains(version, "Percona") { 249 flavor = FlavorPercona 250 } else if strings.Contains(version, "MariaDB") { 251 flavor = FlavorMariaDB 252 } else { 253 // OS distributed MySQL releases have a version string like: 254 // mysqld Ver 5.7.27-0ubuntu0.19.04.1 for Linux on x86_64 ((Ubuntu)) 255 flavor = FlavorMySQL 256 } 257 v := versionRegex.FindStringSubmatch(version) 258 if len(v) != 4 { 259 return flavor, ver, fmt.Errorf("could not parse server version from: %s", version) 260 } 261 ver.Major, err = strconv.Atoi(string(v[1])) 262 if err != nil { 263 return flavor, ver, fmt.Errorf("could not parse server version from: %s", version) 264 } 265 ver.Minor, err = strconv.Atoi(string(v[2])) 266 if err != nil { 267 return flavor, ver, fmt.Errorf("could not parse server version from: %s", version) 268 } 269 ver.Patch, err = strconv.Atoi(string(v[3])) 270 if err != nil { 271 return flavor, ver, fmt.Errorf("could not parse server version from: %s", version) 272 } 273 274 return 275 } 276 277 // RunMysqlUpgrade will run the mysql_upgrade program on the current 278 // install. Will be called only when mysqld is running with no 279 // network and no grant tables. 280 func (mysqld *Mysqld) RunMysqlUpgrade() error { 281 // Execute as remote action on mysqlctld if requested. 282 if socketFile != "" { 283 log.Infof("executing Mysqld.RunMysqlUpgrade() remotely via mysqlctld server: %v", socketFile) 284 client, err := mysqlctlclient.New("unix", socketFile) 285 if err != nil { 286 return fmt.Errorf("can't dial mysqlctld: %v", err) 287 } 288 defer client.Close() 289 return client.RunMysqlUpgrade(context.TODO()) 290 } 291 292 if mysqld.capabilities.hasMySQLUpgradeInServer() { 293 log.Warningf("MySQL version has built-in upgrade, skipping RunMySQLUpgrade") 294 return nil 295 } 296 297 // Since we started mysql with --skip-grant-tables, we should 298 // be able to run mysql_upgrade without any valid user or 299 // password. However, mysql_upgrade executes a 'flush 300 // privileges' right in the middle, and then subsequent 301 // commands fail if we don't use valid credentials. So let's 302 // use dba credentials. 303 params, err := mysqld.dbcfgs.DbaConnector().MysqlParams() 304 if err != nil { 305 return err 306 } 307 defaultsFile, err := mysqld.defaultsExtraFile(params) 308 if err != nil { 309 return err 310 } 311 defer os.Remove(defaultsFile) 312 313 // Run the program, if it fails, we fail. Note in this 314 // moment, mysqld is running with no grant tables on the local 315 // socket only, so this doesn't need any user or password. 316 args := []string{ 317 // --defaults-file=* must be the first arg. 318 "--defaults-file=" + defaultsFile, 319 "--force", // Don't complain if it's already been upgraded. 320 } 321 322 // Find mysql_upgrade. If not there, we do nothing. 323 vtMysqlRoot, err := vtenv.VtMysqlRoot() 324 if err != nil { 325 log.Warningf("VT_MYSQL_ROOT not set, skipping mysql_upgrade step: %v", err) 326 return nil 327 } 328 name, err := binaryPath(vtMysqlRoot, "mysql_upgrade") 329 if err != nil { 330 log.Warningf("mysql_upgrade binary not present, skipping it: %v", err) 331 return nil 332 } 333 334 env, err := buildLdPaths() 335 if err != nil { 336 log.Warningf("skipping mysql_upgrade step: %v", err) 337 return nil 338 } 339 340 _, _, err = execCmd(name, args, env, "", nil) 341 return err 342 } 343 344 // Start will start the mysql daemon, either by running the 345 // 'mysqld_start' hook, or by running mysqld_safe in the background. 346 // If a mysqlctld address is provided in a flag, Start will run 347 // remotely. When waiting for mysqld to start, we will use 348 // the dba user. 349 func (mysqld *Mysqld) Start(ctx context.Context, cnf *Mycnf, mysqldArgs ...string) error { 350 // Execute as remote action on mysqlctld if requested. 351 if socketFile != "" { 352 log.Infof("executing Mysqld.Start() remotely via mysqlctld server: %v", socketFile) 353 client, err := mysqlctlclient.New("unix", socketFile) 354 if err != nil { 355 return fmt.Errorf("can't dial mysqlctld: %v", err) 356 } 357 defer client.Close() 358 return client.Start(ctx, mysqldArgs...) 359 } 360 361 if err := mysqld.startNoWait(ctx, cnf, mysqldArgs...); err != nil { 362 return err 363 } 364 365 return mysqld.Wait(ctx, cnf) 366 } 367 368 // startNoWait is the internal version of Start, and it doesn't wait. 369 func (mysqld *Mysqld) startNoWait(ctx context.Context, cnf *Mycnf, mysqldArgs ...string) error { 370 var name string 371 ts := fmt.Sprintf("Mysqld.Start(%v)", time.Now().Unix()) 372 373 // try the mysqld start hook, if any 374 switch hr := hook.NewHook("mysqld_start", mysqldArgs).Execute(); hr.ExitStatus { 375 case hook.HOOK_SUCCESS: 376 // hook exists and worked, we can keep going 377 name = "mysqld_start hook" // nolint 378 case hook.HOOK_DOES_NOT_EXIST: 379 // hook doesn't exist, run mysqld_safe ourselves 380 log.Infof("%v: No mysqld_start hook, running mysqld_safe directly", ts) 381 vtMysqlRoot, err := vtenv.VtMysqlRoot() 382 if err != nil { 383 return err 384 } 385 name, err = binaryPath(vtMysqlRoot, "mysqld_safe") 386 if err != nil { 387 // The movement to use systemd means that mysqld_safe is not always provided. 388 // This should not be considered an issue do not generate a warning. 389 log.Infof("%v: trying to launch mysqld instead", err) 390 name, err = binaryPath(vtMysqlRoot, "mysqld") 391 // If this also fails, return an error. 392 if err != nil { 393 return err 394 } 395 } 396 mysqlBaseDir, err := vtenv.VtMysqlBaseDir() 397 if err != nil { 398 return err 399 } 400 args := []string{ 401 "--defaults-file=" + cnf.Path, 402 "--basedir=" + mysqlBaseDir, 403 } 404 args = append(args, mysqldArgs...) 405 env, err := buildLdPaths() 406 if err != nil { 407 return err 408 } 409 410 cmd := exec.Command(name, args...) 411 cmd.Dir = vtMysqlRoot 412 cmd.Env = env 413 log.Infof("%v %#v", ts, cmd) 414 stderr, err := cmd.StderrPipe() 415 if err != nil { 416 return err 417 } 418 stdout, err := cmd.StdoutPipe() 419 if err != nil { 420 return err 421 } 422 go func() { 423 scanner := bufio.NewScanner(stderr) 424 for scanner.Scan() { 425 log.Infof("%v stderr: %v", ts, scanner.Text()) 426 } 427 }() 428 go func() { 429 scanner := bufio.NewScanner(stdout) 430 for scanner.Scan() { 431 log.Infof("%v stdout: %v", ts, scanner.Text()) 432 } 433 }() 434 err = cmd.Start() 435 if err != nil { 436 return err 437 } 438 439 mysqld.mutex.Lock() 440 mysqld.cancelWaitCmd = make(chan struct{}) 441 go func(cancel <-chan struct{}) { 442 // Wait regardless of cancel, so we don't generate defunct processes. 443 err := cmd.Wait() 444 log.Infof("%v exit: %v", ts, err) 445 446 // The process exited. Trigger OnTerm callbacks, unless we were cancelled. 447 select { 448 case <-cancel: 449 default: 450 mysqld.mutex.Lock() 451 for _, callback := range mysqld.onTermFuncs { 452 go callback() 453 } 454 mysqld.mutex.Unlock() 455 } 456 }(mysqld.cancelWaitCmd) 457 mysqld.mutex.Unlock() 458 default: 459 // hook failed, we report error 460 return fmt.Errorf("mysqld_start hook failed: %v", hr.String()) 461 } 462 463 return nil 464 } 465 466 // Wait returns nil when mysqld is up and accepting connections. It 467 // will use the dba credentials to try to connect. Use wait() with 468 // different credentials if needed. 469 func (mysqld *Mysqld) Wait(ctx context.Context, cnf *Mycnf) error { 470 params, err := mysqld.dbcfgs.DbaConnector().MysqlParams() 471 if err != nil { 472 return err 473 } 474 475 return mysqld.wait(ctx, cnf, params) 476 } 477 478 // wait is the internal version of Wait, that takes credentials. 479 func (mysqld *Mysqld) wait(ctx context.Context, cnf *Mycnf, params *mysql.ConnParams) error { 480 log.Infof("Waiting for mysqld socket file (%v) to be ready...", cnf.SocketFile) 481 482 for { 483 select { 484 case <-ctx.Done(): 485 return errors.New("deadline exceeded waiting for mysqld socket file to appear: " + cnf.SocketFile) 486 default: 487 } 488 489 _, statErr := os.Stat(cnf.SocketFile) 490 if statErr == nil { 491 // Make sure the socket file isn't stale. 492 conn, connErr := mysql.Connect(ctx, params) 493 if connErr == nil { 494 conn.Close() 495 return nil 496 } 497 log.Infof("mysqld socket file exists, but can't connect: %v", connErr) 498 } else if !os.IsNotExist(statErr) { 499 return fmt.Errorf("can't stat mysqld socket file: %v", statErr) 500 } 501 time.Sleep(1000 * time.Millisecond) 502 } 503 } 504 505 // Shutdown will stop the mysqld daemon that is running in the background. 506 // 507 // waitForMysqld: should the function block until mysqld has stopped? 508 // This can actually take a *long* time if the buffer cache needs to be fully 509 // flushed - on the order of 20-30 minutes. 510 // 511 // If a mysqlctld address is provided in a flag, Shutdown will run remotely. 512 func (mysqld *Mysqld) Shutdown(ctx context.Context, cnf *Mycnf, waitForMysqld bool) error { 513 log.Infof("Mysqld.Shutdown") 514 515 // Execute as remote action on mysqlctld if requested. 516 if socketFile != "" { 517 log.Infof("executing Mysqld.Shutdown() remotely via mysqlctld server: %v", socketFile) 518 client, err := mysqlctlclient.New("unix", socketFile) 519 if err != nil { 520 return fmt.Errorf("can't dial mysqlctld: %v", err) 521 } 522 defer client.Close() 523 return client.Shutdown(ctx, waitForMysqld) 524 } 525 526 // We're shutting down on purpose. We no longer want to be notified when 527 // mysqld terminates. 528 mysqld.mutex.Lock() 529 if mysqld.cancelWaitCmd != nil { 530 close(mysqld.cancelWaitCmd) 531 mysqld.cancelWaitCmd = nil 532 } 533 mysqld.mutex.Unlock() 534 535 // possibly mysql is already shutdown, check for a few files first 536 _, socketPathErr := os.Stat(cnf.SocketFile) 537 _, pidPathErr := os.Stat(cnf.PidFile) 538 if os.IsNotExist(socketPathErr) && os.IsNotExist(pidPathErr) { 539 log.Warningf("assuming mysqld already shut down - no socket, no pid file found") 540 return nil 541 } 542 543 // try the mysqld shutdown hook, if any 544 h := hook.NewSimpleHook("mysqld_shutdown") 545 hr := h.ExecuteContext(ctx) 546 switch hr.ExitStatus { 547 case hook.HOOK_SUCCESS: 548 // hook exists and worked, we can keep going 549 case hook.HOOK_DOES_NOT_EXIST: 550 // hook doesn't exist, try mysqladmin 551 log.Infof("No mysqld_shutdown hook, running mysqladmin directly") 552 dir, err := vtenv.VtMysqlRoot() 553 if err != nil { 554 return err 555 } 556 name, err := binaryPath(dir, "mysqladmin") 557 if err != nil { 558 return err 559 } 560 params, err := mysqld.dbcfgs.DbaConnector().MysqlParams() 561 if err != nil { 562 return err 563 } 564 cnf, err := mysqld.defaultsExtraFile(params) 565 if err != nil { 566 return err 567 } 568 defer os.Remove(cnf) 569 args := []string{ 570 "--defaults-extra-file=" + cnf, 571 "--shutdown-timeout=300", 572 "--connect-timeout=30", 573 "--wait=10", 574 "shutdown", 575 } 576 env, err := buildLdPaths() 577 if err != nil { 578 return err 579 } 580 if _, _, err = execCmd(name, args, env, dir, nil); err != nil { 581 return err 582 } 583 default: 584 // hook failed, we report error 585 return fmt.Errorf("mysqld_shutdown hook failed: %v", hr.String()) 586 } 587 588 // Wait for mysqld to really stop. Use the socket and pid files as a 589 // proxy for that since we can't call wait() in a process we 590 // didn't start. 591 if waitForMysqld { 592 log.Infof("Mysqld.Shutdown: waiting for socket file (%v) and pid file (%v) to disappear", 593 cnf.SocketFile, cnf.PidFile) 594 595 for { 596 select { 597 case <-ctx.Done(): 598 return errors.New("gave up waiting for mysqld to stop") 599 default: 600 } 601 602 _, socketPathErr = os.Stat(cnf.SocketFile) 603 _, pidPathErr = os.Stat(cnf.PidFile) 604 if os.IsNotExist(socketPathErr) && os.IsNotExist(pidPathErr) { 605 return nil 606 } 607 time.Sleep(100 * time.Millisecond) 608 } 609 } 610 return nil 611 } 612 613 // execCmd searches the PATH for a command and runs it, logging the output. 614 // If input is not nil, pipe it to the command's stdin. 615 func execCmd(name string, args, env []string, dir string, input io.Reader) (cmd *exec.Cmd, output string, err error) { 616 cmdPath, _ := exec.LookPath(name) 617 log.Infof("execCmd: %v %v %v", name, cmdPath, args) 618 619 cmd = exec.Command(cmdPath, args...) 620 cmd.Env = env 621 cmd.Dir = dir 622 if input != nil { 623 cmd.Stdin = input 624 } 625 out, err := cmd.CombinedOutput() 626 output = string(out) 627 if err != nil { 628 log.Infof("execCmd: %v failed: %v", name, err) 629 err = fmt.Errorf("%v: %v, output: %v", name, err, output) 630 } 631 log.Infof("execCmd: %v output: %v", name, output) 632 return cmd, output, err 633 } 634 635 // binaryPath does a limited path lookup for a command, 636 // searching only within sbin and bin in the given root. 637 func binaryPath(root, binary string) (string, error) { 638 subdirs := []string{"sbin", "bin", "libexec", "scripts"} 639 for _, subdir := range subdirs { 640 binPath := path.Join(root, subdir, binary) 641 if _, err := os.Stat(binPath); err == nil { 642 return binPath, nil 643 } 644 } 645 return "", fmt.Errorf("%s not found in any of %s/{%s}", 646 binary, root, strings.Join(subdirs, ",")) 647 } 648 649 // InitConfig will create the default directory structure for the mysqld process, 650 // generate / configure a my.cnf file. 651 func (mysqld *Mysqld) InitConfig(cnf *Mycnf) error { 652 log.Infof("mysqlctl.InitConfig") 653 err := mysqld.createDirs(cnf) 654 if err != nil { 655 log.Errorf("%s", err.Error()) 656 return err 657 } 658 // Set up config files. 659 if err = mysqld.initConfig(cnf, cnf.Path); err != nil { 660 log.Errorf("failed creating %v: %v", cnf.Path, err) 661 return err 662 } 663 return nil 664 } 665 666 // Init will create the default directory structure for the mysqld process, 667 // generate / configure a my.cnf file install a skeleton database, 668 // and apply the provided initial SQL file. 669 func (mysqld *Mysqld) Init(ctx context.Context, cnf *Mycnf, initDBSQLFile string) error { 670 log.Infof("mysqlctl.Init") 671 err := mysqld.InitConfig(cnf) 672 if err != nil { 673 log.Errorf("%s", err.Error()) 674 return err 675 } 676 // Install data dir. 677 if err = mysqld.installDataDir(cnf); err != nil { 678 return err 679 } 680 681 // Start mysqld. We do not use Start, as we have to wait using 682 // the root user. 683 if err = mysqld.startNoWait(ctx, cnf); err != nil { 684 log.Errorf("failed starting mysqld: %v\n%v", err, readTailOfMysqldErrorLog(cnf.ErrorLogPath)) 685 return err 686 } 687 688 // Wait for mysqld to be ready, using root credentials, as no 689 // user is created yet. 690 params := &mysql.ConnParams{ 691 Uname: "root", 692 UnixSocket: cnf.SocketFile, 693 } 694 if err = mysqld.wait(ctx, cnf, params); err != nil { 695 log.Errorf("failed starting mysqld in time: %v\n%v", err, readTailOfMysqldErrorLog(cnf.ErrorLogPath)) 696 return err 697 } 698 699 if initDBSQLFile == "" { // default to built-in 700 if err := mysqld.executeMysqlScript(params, strings.NewReader(config.DefaultInitDB)); err != nil { 701 return fmt.Errorf("failed to initialize mysqld: %v", err) 702 } 703 return nil 704 } 705 706 // else, user specified an init db file 707 sqlFile, err := os.Open(initDBSQLFile) 708 if err != nil { 709 return fmt.Errorf("can't open init_db_sql_file (%v): %v", initDBSQLFile, err) 710 } 711 defer sqlFile.Close() 712 if err := mysqld.executeMysqlScript(params, sqlFile); err != nil { 713 return fmt.Errorf("can't run init_db_sql_file (%v): %v", initDBSQLFile, err) 714 } 715 return nil 716 } 717 718 // For debugging purposes show the last few lines of the MySQL error log. 719 // Return a suggestion (string) if the file is non regular or can not be opened. 720 // This helps prevent cases where the error log is symlinked to /dev/stderr etc, 721 // In which case the user can manually open the file. 722 func readTailOfMysqldErrorLog(fileName string) string { 723 fileInfo, err := os.Stat(fileName) 724 if err != nil { 725 return fmt.Sprintf("could not stat mysql error log (%v): %v", fileName, err) 726 } 727 if !fileInfo.Mode().IsRegular() { 728 return fmt.Sprintf("mysql error log file is not a regular file: %v", fileName) 729 } 730 file, err := os.Open(fileName) 731 if err != nil { 732 return fmt.Sprintf("could not open mysql error log (%v): %v", fileName, err) 733 } 734 defer file.Close() 735 startPos := int64(0) 736 if fileInfo.Size() > maxLogFileSampleSize { 737 startPos = fileInfo.Size() - maxLogFileSampleSize 738 } 739 // Show the last few KB of the MySQL error log. 740 buf := make([]byte, maxLogFileSampleSize) 741 flen, err := file.ReadAt(buf, startPos) 742 if err != nil && err != io.EOF { 743 return fmt.Sprintf("could not read mysql error log (%v): %v", fileName, err) 744 } 745 return fmt.Sprintf("tail of mysql error log (%v):\n%s", fileName, buf[:flen]) 746 } 747 748 func (mysqld *Mysqld) installDataDir(cnf *Mycnf) error { 749 mysqlRoot, err := vtenv.VtMysqlRoot() 750 if err != nil { 751 return err 752 } 753 mysqldPath, err := binaryPath(mysqlRoot, "mysqld") 754 if err != nil { 755 return err 756 } 757 758 mysqlBaseDir, err := vtenv.VtMysqlBaseDir() 759 if err != nil { 760 return err 761 } 762 if mysqld.capabilities.hasInitializeInServer() { 763 log.Infof("Installing data dir with mysqld --initialize-insecure") 764 args := []string{ 765 "--defaults-file=" + cnf.Path, 766 "--basedir=" + mysqlBaseDir, 767 "--initialize-insecure", // Use empty 'root'@'localhost' password. 768 } 769 if _, _, err = execCmd(mysqldPath, args, nil, mysqlRoot, nil); err != nil { 770 log.Errorf("mysqld --initialize-insecure failed: %v\n%v", err, readTailOfMysqldErrorLog(cnf.ErrorLogPath)) 771 return err 772 } 773 return nil 774 } 775 776 log.Infof("Installing data dir with mysql_install_db") 777 args := []string{ 778 "--defaults-file=" + cnf.Path, 779 "--basedir=" + mysqlBaseDir, 780 } 781 if mysqld.capabilities.hasMaria104InstallDb() { 782 args = append(args, "--auth-root-authentication-method=normal") 783 } 784 cmdPath, err := binaryPath(mysqlRoot, "mysql_install_db") 785 if err != nil { 786 return err 787 } 788 if _, _, err = execCmd(cmdPath, args, nil, mysqlRoot, nil); err != nil { 789 log.Errorf("mysql_install_db failed: %v\n%v", err, readTailOfMysqldErrorLog(cnf.ErrorLogPath)) 790 return err 791 } 792 return nil 793 } 794 795 func (mysqld *Mysqld) initConfig(cnf *Mycnf, outFile string) error { 796 var err error 797 var configData string 798 799 env := make(map[string]string) 800 envVars := []string{"KEYSPACE", "SHARD", "TABLET_TYPE", "TABLET_ID", "TABLET_DIR", "MYSQL_PORT"} 801 for _, v := range envVars { 802 env[v] = os.Getenv(v) 803 } 804 805 switch hr := hook.NewHookWithEnv("make_mycnf", nil, env).Execute(); hr.ExitStatus { 806 case hook.HOOK_DOES_NOT_EXIST: 807 log.Infof("make_mycnf hook doesn't exist, reading template files") 808 configData, err = cnf.makeMycnf(mysqld.getMycnfTemplate()) 809 case hook.HOOK_SUCCESS: 810 configData, err = cnf.fillMycnfTemplate(hr.Stdout) 811 default: 812 return fmt.Errorf("make_mycnf hook failed(%v): %v", hr.ExitStatus, hr.Stderr) 813 } 814 if err != nil { 815 return err 816 } 817 818 return os.WriteFile(outFile, []byte(configData), 0664) 819 } 820 821 func (mysqld *Mysqld) getMycnfTemplate() string { 822 if mycnfTemplateFile != "" { 823 data, err := os.ReadFile(mycnfTemplateFile) 824 if err != nil { 825 log.Fatalf("template file specified by -mysqlctl_mycnf_template could not be read: %v", mycnfTemplateFile) 826 } 827 return string(data) // use only specified template 828 } 829 myTemplateSource := new(bytes.Buffer) 830 myTemplateSource.WriteString("[mysqld]\n") 831 myTemplateSource.WriteString(config.MycnfDefault) 832 833 // database flavor + version specific file. 834 // {flavor}{major}{minor}.cnf 835 f := FlavorMariaDB 836 if mysqld.capabilities.isMySQLLike() { 837 f = FlavorMySQL 838 } 839 var versionConfig string 840 switch f { 841 case FlavorPercona, FlavorMySQL: 842 switch mysqld.capabilities.version.Major { 843 case 5: 844 if mysqld.capabilities.version.Minor == 7 { 845 versionConfig = config.MycnfMySQL57 846 } else { 847 log.Infof("this version of Vitess does not include built-in support for %v %v", mysqld.capabilities.flavor, mysqld.capabilities.version) 848 } 849 case 8: 850 versionConfig = config.MycnfMySQL80 851 default: 852 log.Infof("this version of Vitess does not include built-in support for %v %v", mysqld.capabilities.flavor, mysqld.capabilities.version) 853 } 854 case FlavorMariaDB: 855 switch mysqld.capabilities.version.Major { 856 case 10: 857 versionConfig = config.MycnfMariaDB10 858 default: 859 log.Infof("this version of Vitess does not include built-in support for %v %v", mysqld.capabilities.flavor, mysqld.capabilities.version) 860 } 861 } 862 863 myTemplateSource.WriteString(versionConfig) 864 865 if extraCnf := os.Getenv("EXTRA_MY_CNF"); extraCnf != "" { 866 parts := strings.Split(extraCnf, ":") 867 for _, path := range parts { 868 data, dataErr := os.ReadFile(path) 869 if dataErr != nil { 870 log.Infof("could not open config file for mycnf: %v", path) 871 continue 872 } 873 myTemplateSource.WriteString("## " + path + "\n") 874 myTemplateSource.Write(data) 875 } 876 } 877 return myTemplateSource.String() 878 } 879 880 // RefreshConfig attempts to recreate the my.cnf from templates, and log and 881 // swap in to place if it's updated. It keeps a copy of the last version in case fallback is required. 882 // Should be called from a stable replica, server_id is not regenerated. 883 func (mysqld *Mysqld) RefreshConfig(ctx context.Context, cnf *Mycnf) error { 884 // Execute as remote action on mysqlctld if requested. 885 if socketFile != "" { 886 log.Infof("executing Mysqld.RefreshConfig() remotely via mysqlctld server: %v", socketFile) 887 client, err := mysqlctlclient.New("unix", socketFile) 888 if err != nil { 889 return fmt.Errorf("can't dial mysqlctld: %v", err) 890 } 891 defer client.Close() 892 return client.RefreshConfig(ctx) 893 } 894 895 log.Info("Checking for updates to my.cnf") 896 f, err := os.CreateTemp(path.Dir(cnf.Path), "my.cnf") 897 if err != nil { 898 return fmt.Errorf("could not create temp file: %v", err) 899 } 900 901 defer os.Remove(f.Name()) 902 err = mysqld.initConfig(cnf, f.Name()) 903 if err != nil { 904 return fmt.Errorf("could not initConfig in %v: %v", f.Name(), err) 905 } 906 907 existing, err := os.ReadFile(cnf.Path) 908 if err != nil { 909 return fmt.Errorf("could not read existing file %v: %v", cnf.Path, err) 910 } 911 updated, err := os.ReadFile(f.Name()) 912 if err != nil { 913 return fmt.Errorf("could not read updated file %v: %v", f.Name(), err) 914 } 915 916 if bytes.Equal(existing, updated) { 917 log.Infof("No changes to my.cnf. Continuing.") 918 return nil 919 } 920 921 backupPath := cnf.Path + ".previous" 922 err = os.Rename(cnf.Path, backupPath) 923 if err != nil { 924 return fmt.Errorf("could not back up existing %v: %v", cnf.Path, err) 925 } 926 err = os.Rename(f.Name(), cnf.Path) 927 if err != nil { 928 return fmt.Errorf("could not move %v to %v: %v", f.Name(), cnf.Path, err) 929 } 930 log.Infof("Updated my.cnf. Backup of previous version available in %v", backupPath) 931 932 return nil 933 } 934 935 // ReinitConfig updates the config file as if Mysqld is initializing. At the 936 // moment it only randomizes ServerID because it's not safe to restore a replica 937 // from a backup and then give it the same ServerID as before, MySQL can then 938 // skip transactions in the replication stream with the same server_id. 939 func (mysqld *Mysqld) ReinitConfig(ctx context.Context, cnf *Mycnf) error { 940 log.Infof("Mysqld.ReinitConfig") 941 942 // Execute as remote action on mysqlctld if requested. 943 if socketFile != "" { 944 log.Infof("executing Mysqld.ReinitConfig() remotely via mysqlctld server: %v", socketFile) 945 client, err := mysqlctlclient.New("unix", socketFile) 946 if err != nil { 947 return fmt.Errorf("can't dial mysqlctld: %v", err) 948 } 949 defer client.Close() 950 return client.ReinitConfig(ctx) 951 } 952 953 if err := cnf.RandomizeMysqlServerID(); err != nil { 954 return err 955 } 956 return mysqld.initConfig(cnf, cnf.Path) 957 } 958 959 func (mysqld *Mysqld) createDirs(cnf *Mycnf) error { 960 tabletDir := cnf.TabletDir() 961 log.Infof("creating directory %s", tabletDir) 962 if err := os.MkdirAll(tabletDir, os.ModePerm); err != nil { 963 return err 964 } 965 for _, dir := range TopLevelDirs() { 966 if err := mysqld.createTopDir(cnf, dir); err != nil { 967 return err 968 } 969 } 970 for _, dir := range cnf.directoryList() { 971 log.Infof("creating directory %s", dir) 972 if err := os.MkdirAll(dir, os.ModePerm); err != nil { 973 return err 974 } 975 // FIXME(msolomon) validate permissions? 976 } 977 return nil 978 } 979 980 // createTopDir creates a top level directory under TabletDir. 981 // However, if a directory of the same name already exists under 982 // vtenv.VtDataRoot(), it creates a directory named after the tablet 983 // id under that directory, and then creates a symlink under TabletDir 984 // that points to the newly created directory. For example, if 985 // /vt/data is present, it will create the following structure: 986 // /vt/data/vt_xxxx /vt/vt_xxxx/data -> /vt/data/vt_xxxx 987 func (mysqld *Mysqld) createTopDir(cnf *Mycnf, dir string) error { 988 tabletDir := cnf.TabletDir() 989 vtname := path.Base(tabletDir) 990 target := path.Join(vtenv.VtDataRoot(), dir) 991 _, err := os.Lstat(target) 992 if err != nil { 993 if os.IsNotExist(err) { 994 topdir := path.Join(tabletDir, dir) 995 log.Infof("creating directory %s", topdir) 996 return os.MkdirAll(topdir, os.ModePerm) 997 } 998 return err 999 } 1000 linkto := path.Join(target, vtname) 1001 source := path.Join(tabletDir, dir) 1002 log.Infof("creating directory %s", linkto) 1003 err = os.MkdirAll(linkto, os.ModePerm) 1004 if err != nil { 1005 return err 1006 } 1007 log.Infof("creating symlink %s -> %s", source, linkto) 1008 return os.Symlink(linkto, source) 1009 } 1010 1011 // Teardown will shutdown the running daemon, and delete the root directory. 1012 func (mysqld *Mysqld) Teardown(ctx context.Context, cnf *Mycnf, force bool) error { 1013 log.Infof("mysqlctl.Teardown") 1014 if err := mysqld.Shutdown(ctx, cnf, true); err != nil { 1015 log.Warningf("failed mysqld shutdown: %v", err.Error()) 1016 if !force { 1017 return err 1018 } 1019 } 1020 var removalErr error 1021 for _, dir := range TopLevelDirs() { 1022 qdir := path.Join(cnf.TabletDir(), dir) 1023 if err := deleteTopDir(qdir); err != nil { 1024 removalErr = err 1025 } 1026 } 1027 return removalErr 1028 } 1029 1030 func deleteTopDir(dir string) (removalErr error) { 1031 fi, err := os.Lstat(dir) 1032 if err != nil { 1033 log.Errorf("error deleting dir %v: %v", dir, err.Error()) 1034 removalErr = err 1035 } else if fi.Mode()&os.ModeSymlink != 0 { 1036 target, err := filepath.EvalSymlinks(dir) 1037 if err != nil { 1038 log.Errorf("could not resolve symlink %v: %v", dir, err.Error()) 1039 removalErr = err 1040 } 1041 log.Infof("remove data dir (symlinked) %v", target) 1042 if err = os.RemoveAll(target); err != nil { 1043 log.Errorf("failed removing %v: %v", target, err.Error()) 1044 removalErr = err 1045 } 1046 } 1047 log.Infof("remove data dir %v", dir) 1048 if err = os.RemoveAll(dir); err != nil { 1049 log.Errorf("failed removing %v: %v", dir, err.Error()) 1050 removalErr = err 1051 } 1052 return 1053 } 1054 1055 // executeMysqlScript executes a .sql script from an io.Reader with the mysql 1056 // command line tool. It uses the connParams as is, not adding credentials. 1057 func (mysqld *Mysqld) executeMysqlScript(connParams *mysql.ConnParams, sql io.Reader) error { 1058 dir, err := vtenv.VtMysqlRoot() 1059 if err != nil { 1060 return err 1061 } 1062 name, err := binaryPath(dir, "mysql") 1063 if err != nil { 1064 return err 1065 } 1066 cnf, err := mysqld.defaultsExtraFile(connParams) 1067 if err != nil { 1068 return err 1069 } 1070 defer os.Remove(cnf) 1071 args := []string{ 1072 "--defaults-extra-file=" + cnf, 1073 "--batch", 1074 } 1075 env, err := buildLdPaths() 1076 if err != nil { 1077 return err 1078 } 1079 _, _, err = execCmd(name, args, env, dir, sql) 1080 if err != nil { 1081 return err 1082 } 1083 return nil 1084 } 1085 1086 // defaultsExtraFile returns the filename for a temporary config file 1087 // that contains the user, password and socket file to connect to 1088 // mysqld. We write a temporary config file so the password is never 1089 // passed as a command line parameter. Note os.CreateTemp uses 0600 1090 // as permissions, so only the local user can read the file. The 1091 // returned temporary file should be removed after use, typically in a 1092 // 'defer os.Remove()' statement. 1093 func (mysqld *Mysqld) defaultsExtraFile(connParams *mysql.ConnParams) (string, error) { 1094 var contents string 1095 connParams.Pass = strings.Replace(connParams.Pass, "#", "\\#", -1) 1096 if connParams.UnixSocket == "" { 1097 contents = fmt.Sprintf(` 1098 [client] 1099 user=%v 1100 password=%v 1101 host=%v 1102 port=%v 1103 `, connParams.Uname, connParams.Pass, connParams.Host, connParams.Port) 1104 } else { 1105 contents = fmt.Sprintf(` 1106 [client] 1107 user=%v 1108 password=%v 1109 socket=%v 1110 `, connParams.Uname, connParams.Pass, connParams.UnixSocket) 1111 } 1112 1113 tmpfile, err := os.CreateTemp("", "example") 1114 if err != nil { 1115 return "", err 1116 } 1117 name := tmpfile.Name() 1118 if _, err := tmpfile.Write([]byte(contents)); err != nil { 1119 tmpfile.Close() 1120 os.Remove(name) 1121 return "", err 1122 } 1123 if err := tmpfile.Close(); err != nil { 1124 os.Remove(name) 1125 return "", err 1126 } 1127 return name, nil 1128 } 1129 1130 // GetAppConnection returns a connection from the app pool. 1131 // Recycle needs to be called on the result. 1132 func (mysqld *Mysqld) GetAppConnection(ctx context.Context) (*dbconnpool.PooledDBConnection, error) { 1133 return mysqld.appPool.Get(ctx) 1134 } 1135 1136 // GetDbaConnection creates a new DBConnection. 1137 func (mysqld *Mysqld) GetDbaConnection(ctx context.Context) (*dbconnpool.DBConnection, error) { 1138 return dbconnpool.NewDBConnection(ctx, mysqld.dbcfgs.DbaConnector()) 1139 } 1140 1141 // GetAllPrivsConnection creates a new DBConnection. 1142 func (mysqld *Mysqld) GetAllPrivsConnection(ctx context.Context) (*dbconnpool.DBConnection, error) { 1143 return dbconnpool.NewDBConnection(ctx, mysqld.dbcfgs.AllPrivsWithDB()) 1144 } 1145 1146 // Close will close this instance of Mysqld. It will wait for all dba 1147 // queries to be finished. 1148 func (mysqld *Mysqld) Close() { 1149 if mysqld.dbaPool != nil { 1150 mysqld.dbaPool.Close() 1151 } 1152 if mysqld.appPool != nil { 1153 mysqld.appPool.Close() 1154 } 1155 } 1156 1157 // OnTerm registers a function to be called if mysqld terminates for any 1158 // reason other than a call to Mysqld.Shutdown(). This only works if mysqld 1159 // was actually started by calling Start() on this Mysqld instance. 1160 func (mysqld *Mysqld) OnTerm(f func()) { 1161 mysqld.mutex.Lock() 1162 defer mysqld.mutex.Unlock() 1163 mysqld.onTermFuncs = append(mysqld.onTermFuncs, f) 1164 } 1165 1166 func buildLdPaths() ([]string, error) { 1167 vtMysqlRoot, err := vtenv.VtMysqlRoot() 1168 if err != nil { 1169 return []string{}, err 1170 } 1171 1172 ldPaths := []string{ 1173 fmt.Sprintf("LD_LIBRARY_PATH=%s/lib/mysql", vtMysqlRoot), 1174 os.ExpandEnv("LD_PRELOAD=$LD_PRELOAD"), 1175 } 1176 1177 return ldPaths, nil 1178 } 1179 1180 // GetVersionString is part of the MysqlDeamon interface. 1181 func (mysqld *Mysqld) GetVersionString() string { 1182 return fmt.Sprintf("%d.%d.%d", mysqld.capabilities.version.Major, mysqld.capabilities.version.Minor, mysqld.capabilities.version.Patch) 1183 } 1184 1185 // GetVersionComment gets the version comment. 1186 func (mysqld *Mysqld) GetVersionComment(ctx context.Context) string { 1187 qr, err := mysqld.FetchSuperQuery(ctx, "select @@global.version_comment") 1188 if err != nil { 1189 return "" 1190 } 1191 if len(qr.Rows) != 1 { 1192 return "" 1193 } 1194 res := qr.Named().Row() 1195 versionComment, _ := res.ToString("@@global.version_comment") 1196 return versionComment 1197 } 1198 1199 // applyBinlogFile extracts a binary log file and applies it to MySQL. It is the equivalent of: 1200 // $ mysqlbinlog --include-gtids binlog.file | mysql 1201 func (mysqld *Mysqld) applyBinlogFile(binlogFile string, includeGTIDs mysql.GTIDSet) error { 1202 var pipe io.ReadCloser 1203 var mysqlbinlogCmd *exec.Cmd 1204 var mysqlCmd *exec.Cmd 1205 1206 dir, err := vtenv.VtMysqlRoot() 1207 if err != nil { 1208 return err 1209 } 1210 env, err := buildLdPaths() 1211 if err != nil { 1212 return err 1213 } 1214 { 1215 name, err := binaryPath(dir, "mysqlbinlog") 1216 if err != nil { 1217 return err 1218 } 1219 args := []string{} 1220 if gtids := includeGTIDs.String(); gtids != "" { 1221 args = append(args, 1222 "--include-gtids", 1223 gtids, 1224 ) 1225 } 1226 args = append(args, binlogFile) 1227 1228 mysqlbinlogCmd = exec.Command(name, args...) 1229 mysqlbinlogCmd.Dir = dir 1230 mysqlbinlogCmd.Env = env 1231 log.Infof("applyBinlogFile: running %#v", mysqlbinlogCmd) 1232 pipe, err = mysqlbinlogCmd.StdoutPipe() // to be piped into mysql 1233 if err != nil { 1234 return err 1235 } 1236 } 1237 { 1238 name, err := binaryPath(dir, "mysql") 1239 if err != nil { 1240 return err 1241 } 1242 params, err := mysqld.dbcfgs.DbaConnector().MysqlParams() 1243 if err != nil { 1244 return err 1245 } 1246 cnf, err := mysqld.defaultsExtraFile(params) 1247 if err != nil { 1248 return err 1249 } 1250 defer os.Remove(cnf) 1251 args := []string{ 1252 "--defaults-extra-file=" + cnf, 1253 } 1254 mysqlCmd = exec.Command(name, args...) 1255 mysqlCmd.Dir = dir 1256 mysqlCmd.Env = env 1257 mysqlCmd.Stdin = pipe // piped from mysqlbinlog 1258 } 1259 // Run both processes, piped: 1260 if err := mysqlbinlogCmd.Start(); err != nil { 1261 return err 1262 } 1263 if err := mysqlCmd.Start(); err != nil { 1264 return err 1265 } 1266 // Wait for both to complete: 1267 if err := mysqlbinlogCmd.Wait(); err != nil { 1268 return err 1269 } 1270 if err := mysqlCmd.Wait(); err != nil { 1271 return err 1272 } 1273 return nil 1274 }