manager.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. // Copyright 2021 EMQ Technologies Co., Ltd.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // Manage the loading of both native and portable plugins
  15. package native
  16. import (
  17. "archive/zip"
  18. "bytes"
  19. "fmt"
  20. "github.com/lf-edge/ekuiper/internal/conf"
  21. "github.com/lf-edge/ekuiper/internal/meta"
  22. "github.com/lf-edge/ekuiper/internal/pkg/filex"
  23. "github.com/lf-edge/ekuiper/internal/pkg/httpx"
  24. "github.com/lf-edge/ekuiper/internal/pkg/store"
  25. plugin2 "github.com/lf-edge/ekuiper/internal/plugin"
  26. "github.com/lf-edge/ekuiper/pkg/api"
  27. "github.com/lf-edge/ekuiper/pkg/errorx"
  28. "github.com/lf-edge/ekuiper/pkg/kv"
  29. "io/ioutil"
  30. "os"
  31. "os/exec"
  32. "path"
  33. "path/filepath"
  34. "plugin"
  35. "regexp"
  36. "strings"
  37. "sync"
  38. "time"
  39. "unicode"
  40. )
  41. // Manager Initialized in the binder
  42. var manager *Manager
  43. const DELETED = "$deleted"
  44. //Manager is append only because plugin cannot delete or reload. To delete a plugin, restart the server to reindex
  45. type Manager struct {
  46. sync.RWMutex
  47. // 3 maps for source/sink/function. In each map, key is the plugin name, value is the version
  48. plugins []map[string]string
  49. // A map from function name to its plugin file name. It is constructed during initialization by reading kv info. All functions must have at least an entry, even the function resizes in a one function plugin.
  50. symbols map[string]string
  51. // loaded symbols in current runtime
  52. runtime map[string]plugin.Symbol
  53. // dirs
  54. pluginDir string
  55. etcDir string
  56. // the access to db
  57. db kv.KeyValue
  58. }
  59. // InitManager must only be called once
  60. func InitManager() (*Manager, error) {
  61. pluginDir, err := conf.GetPluginsLoc()
  62. if err != nil {
  63. return nil, fmt.Errorf("cannot find plugins folder: %s", err)
  64. }
  65. etcDir, err := conf.GetConfLoc()
  66. if err != nil {
  67. return nil, fmt.Errorf("cannot find etc folder: %s", err)
  68. }
  69. err, db := store.GetKV("pluginFuncs")
  70. if err != nil {
  71. return nil, fmt.Errorf("error when opening db: %v", err)
  72. }
  73. plugins := make([]map[string]string, 3)
  74. for i := range plugin2.PluginTypes {
  75. names, err := findAll(plugin2.PluginType(i), pluginDir)
  76. if err != nil {
  77. return nil, fmt.Errorf("fail to find existing plugins: %s", err)
  78. }
  79. plugins[i] = names
  80. }
  81. registry := &Manager{plugins: plugins, symbols: make(map[string]string), db: db, pluginDir: pluginDir, etcDir: etcDir, runtime: make(map[string]plugin.Symbol)}
  82. for pf := range plugins[plugin2.FUNCTION] {
  83. l := make([]string, 0)
  84. if ok, err := db.Get(pf, &l); ok {
  85. registry.storeSymbols(pf, l)
  86. } else if err != nil {
  87. return nil, fmt.Errorf("error when querying kv: %s", err)
  88. } else {
  89. registry.storeSymbols(pf, []string{pf})
  90. }
  91. }
  92. manager = registry
  93. return registry, nil
  94. }
  95. func findAll(t plugin2.PluginType, pluginDir string) (result map[string]string, err error) {
  96. result = make(map[string]string)
  97. dir := path.Join(pluginDir, plugin2.PluginTypes[t])
  98. files, err := ioutil.ReadDir(dir)
  99. if err != nil {
  100. return
  101. }
  102. for _, file := range files {
  103. baseName := filepath.Base(file.Name())
  104. if strings.HasSuffix(baseName, ".so") {
  105. n, v := parseName(baseName)
  106. result[n] = v
  107. }
  108. }
  109. return
  110. }
  111. func GetManager() *Manager {
  112. return manager
  113. }
  114. func (rr *Manager) get(t plugin2.PluginType, name string) (string, bool) {
  115. rr.RLock()
  116. result := rr.plugins[t]
  117. rr.RUnlock()
  118. r, ok := result[name]
  119. return r, ok
  120. }
  121. func (rr *Manager) store(t plugin2.PluginType, name string, version string) {
  122. rr.Lock()
  123. rr.plugins[t][name] = version
  124. rr.Unlock()
  125. }
  126. func (rr *Manager) storeSymbols(name string, symbols []string) error {
  127. rr.Lock()
  128. defer rr.Unlock()
  129. for _, s := range symbols {
  130. if _, ok := rr.symbols[s]; ok {
  131. return fmt.Errorf("function name %s already exists", s)
  132. } else {
  133. rr.symbols[s] = name
  134. }
  135. }
  136. return nil
  137. }
  138. func (rr *Manager) removeSymbols(symbols []string) {
  139. rr.Lock()
  140. for _, s := range symbols {
  141. delete(rr.symbols, s)
  142. }
  143. rr.Unlock()
  144. }
  145. // API for management
  146. func (rr *Manager) List(t plugin2.PluginType) []string {
  147. rr.RLock()
  148. result := rr.plugins[t]
  149. rr.RUnlock()
  150. keys := make([]string, 0, len(result))
  151. for k := range result {
  152. keys = append(keys, k)
  153. }
  154. return keys
  155. }
  156. func (rr *Manager) ListSymbols() []string {
  157. rr.RLock()
  158. result := rr.symbols
  159. rr.RUnlock()
  160. keys := make([]string, 0, len(result))
  161. for k := range result {
  162. keys = append(keys, k)
  163. }
  164. return keys
  165. }
  166. func (rr *Manager) GetPluginVersionBySymbol(t plugin2.PluginType, symbolName string) (string, bool) {
  167. switch t {
  168. case plugin2.FUNCTION:
  169. rr.RLock()
  170. result := rr.plugins[t]
  171. name, ok := rr.symbols[symbolName]
  172. rr.RUnlock()
  173. if ok {
  174. r, nok := result[name]
  175. return r, nok
  176. } else {
  177. return "", false
  178. }
  179. default:
  180. return rr.get(t, symbolName)
  181. }
  182. }
  183. func (rr *Manager) GetPluginBySymbol(t plugin2.PluginType, symbolName string) (string, bool) {
  184. switch t {
  185. case plugin2.FUNCTION:
  186. rr.RLock()
  187. defer rr.RUnlock()
  188. name, ok := rr.symbols[symbolName]
  189. return name, ok
  190. default:
  191. return symbolName, true
  192. }
  193. }
  194. func (rr *Manager) Register(t plugin2.PluginType, j plugin2.Plugin) error {
  195. name, uri, shellParas := j.GetName(), j.GetFile(), j.GetShellParas()
  196. //Validation
  197. name = strings.Trim(name, " ")
  198. if name == "" {
  199. return fmt.Errorf("invalid name %s: should not be empty", name)
  200. }
  201. if !httpx.IsValidUrl(uri) || !strings.HasSuffix(uri, ".zip") {
  202. return fmt.Errorf("invalid uri %s", uri)
  203. }
  204. if v, ok := rr.get(t, name); ok {
  205. if v == DELETED {
  206. return fmt.Errorf("invalid name %s: the plugin is marked as deleted but Kuiper is not restarted for the change to take effect yet", name)
  207. } else {
  208. return fmt.Errorf("invalid name %s: duplicate", name)
  209. }
  210. }
  211. var err error
  212. if t == plugin2.FUNCTION {
  213. if len(j.GetSymbols()) > 0 {
  214. err = rr.db.Set(name, j.GetSymbols())
  215. if err != nil {
  216. return err
  217. }
  218. err = rr.storeSymbols(name, j.GetSymbols())
  219. } else {
  220. err = rr.storeSymbols(name, []string{name})
  221. }
  222. }
  223. if err != nil {
  224. return err
  225. }
  226. zipPath := path.Join(rr.pluginDir, name+".zip")
  227. var unzipFiles []string
  228. //clean up: delete zip file and unzip files in error
  229. defer os.Remove(zipPath)
  230. //download
  231. err = httpx.DownloadFile(zipPath, uri)
  232. if err != nil {
  233. return fmt.Errorf("fail to download file %s: %s", uri, err)
  234. }
  235. //unzip and copy to destination
  236. unzipFiles, version, err := rr.install(t, name, zipPath, shellParas)
  237. if err == nil && len(j.GetSymbols()) > 0 {
  238. err = rr.db.Set(name, j.GetSymbols())
  239. }
  240. if err != nil { //Revert for any errors
  241. if t == plugin2.SOURCE && len(unzipFiles) == 1 { //source that only copy so file
  242. os.RemoveAll(unzipFiles[0])
  243. }
  244. if len(j.GetSymbols()) > 0 {
  245. rr.removeSymbols(j.GetSymbols())
  246. } else {
  247. rr.removeSymbols([]string{name})
  248. }
  249. return fmt.Errorf("fail to install plugin: %s", err)
  250. }
  251. rr.store(t, name, version)
  252. switch t {
  253. case plugin2.SINK:
  254. if err := meta.ReadSinkMetaFile(path.Join(rr.etcDir, plugin2.PluginTypes[t], name+`.json`), true); nil != err {
  255. conf.Log.Errorf("readSinkFile:%v", err)
  256. }
  257. case plugin2.SOURCE:
  258. if err := meta.ReadSourceMetaFile(path.Join(rr.etcDir, plugin2.PluginTypes[t], name+`.json`), true); nil != err {
  259. conf.Log.Errorf("readSourceFile:%v", err)
  260. }
  261. case plugin2.FUNCTION:
  262. if err := meta.ReadFuncMetaFile(path.Join(rr.etcDir, plugin2.PluginTypes[t], name+`.json`), true); nil != err {
  263. conf.Log.Errorf("readFuncFile:%v", err)
  264. }
  265. }
  266. return nil
  267. }
  268. // RegisterFuncs prerequisite:function plugin of name exists
  269. func (rr *Manager) RegisterFuncs(name string, functions []string) error {
  270. if len(functions) == 0 {
  271. return fmt.Errorf("property 'functions' must not be empty")
  272. }
  273. old := make([]string, 0)
  274. if ok, err := rr.db.Get(name, &old); err != nil {
  275. return err
  276. } else if ok {
  277. rr.removeSymbols(old)
  278. } else if !ok {
  279. rr.removeSymbols([]string{name})
  280. }
  281. err := rr.db.Set(name, functions)
  282. if err != nil {
  283. return err
  284. }
  285. return rr.storeSymbols(name, functions)
  286. }
  287. func (rr *Manager) Delete(t plugin2.PluginType, name string, stop bool) error {
  288. name = strings.Trim(name, " ")
  289. if name == "" {
  290. return fmt.Errorf("invalid name %s: should not be empty", name)
  291. }
  292. soPath, err := rr.getSoFilePath(t, name, true)
  293. if err != nil {
  294. return err
  295. }
  296. var results []string
  297. paths := []string{
  298. soPath,
  299. }
  300. // Find etc folder
  301. etcPath := path.Join(rr.etcDir, plugin2.PluginTypes[t], name)
  302. if fi, err := os.Stat(etcPath); err == nil {
  303. if fi.Mode().IsDir() {
  304. paths = append(paths, etcPath)
  305. }
  306. }
  307. switch t {
  308. case plugin2.SOURCE:
  309. paths = append(paths, path.Join(rr.etcDir, plugin2.PluginTypes[t], name+".yaml"))
  310. meta.UninstallSource(name)
  311. case plugin2.SINK:
  312. meta.UninstallSink(name)
  313. case plugin2.FUNCTION:
  314. old := make([]string, 0)
  315. if ok, err := rr.db.Get(name, &old); err != nil {
  316. return err
  317. } else if ok {
  318. rr.removeSymbols(old)
  319. err := rr.db.Delete(name)
  320. if err != nil {
  321. return err
  322. }
  323. } else if !ok {
  324. rr.removeSymbols([]string{name})
  325. }
  326. meta.UninstallFunc(name)
  327. }
  328. for _, p := range paths {
  329. _, err := os.Stat(p)
  330. if err == nil {
  331. err = os.RemoveAll(p)
  332. if err != nil {
  333. results = append(results, err.Error())
  334. }
  335. } else {
  336. results = append(results, fmt.Sprintf("can't find %s", p))
  337. }
  338. }
  339. if len(results) > 0 {
  340. return fmt.Errorf(strings.Join(results, "\n"))
  341. } else {
  342. rr.store(t, name, DELETED)
  343. if stop {
  344. go func() {
  345. time.Sleep(1 * time.Second)
  346. os.Exit(100)
  347. }()
  348. }
  349. return nil
  350. }
  351. }
  352. func (rr *Manager) GetPluginInfo(t plugin2.PluginType, name string) (map[string]interface{}, bool) {
  353. v, ok := rr.get(t, name)
  354. if strings.HasPrefix(v, "v") {
  355. v = v[1:]
  356. }
  357. if ok {
  358. r := map[string]interface{}{
  359. "name": name,
  360. "version": v,
  361. }
  362. if t == plugin2.FUNCTION {
  363. l := make([]string, 0)
  364. if ok, _ := rr.db.Get(name, &l); ok {
  365. r["functions"] = l
  366. }
  367. // ignore the error
  368. }
  369. return r, ok
  370. }
  371. return nil, false
  372. }
  373. func (rr *Manager) install(t plugin2.PluginType, name, src string, shellParas []string) ([]string, string, error) {
  374. var filenames []string
  375. var tempPath = path.Join(rr.pluginDir, "temp", plugin2.PluginTypes[t], name)
  376. defer os.RemoveAll(tempPath)
  377. r, err := zip.OpenReader(src)
  378. if err != nil {
  379. return filenames, "", err
  380. }
  381. defer r.Close()
  382. haveInstallFile := false
  383. for _, file := range r.File {
  384. fileName := file.Name
  385. if fileName == "install.sh" {
  386. haveInstallFile = true
  387. }
  388. }
  389. if len(shellParas) != 0 && !haveInstallFile {
  390. return filenames, "", fmt.Errorf("have shell parameters : %s but no install.sh file", shellParas)
  391. }
  392. soPrefix := regexp.MustCompile(fmt.Sprintf(`^((%s)|(%s))(@.*)?\.so$`, name, ucFirst(name)))
  393. var yamlFile, yamlPath, version string
  394. expFiles := 1
  395. if t == plugin2.SOURCE {
  396. yamlFile = name + ".yaml"
  397. yamlPath = path.Join(rr.etcDir, plugin2.PluginTypes[t], yamlFile)
  398. expFiles = 2
  399. }
  400. var revokeFiles []string
  401. for _, file := range r.File {
  402. fileName := file.Name
  403. if yamlFile == fileName {
  404. err = filex.UnzipTo(file, yamlPath)
  405. if err != nil {
  406. return filenames, "", err
  407. }
  408. revokeFiles = append(revokeFiles, yamlPath)
  409. filenames = append(filenames, yamlPath)
  410. } else if fileName == name+".json" {
  411. jsonPath := path.Join(rr.etcDir, plugin2.PluginTypes[t], fileName)
  412. if err := filex.UnzipTo(file, jsonPath); nil != err {
  413. conf.Log.Errorf("Failed to decompress the metadata %s file", fileName)
  414. } else {
  415. revokeFiles = append(revokeFiles, jsonPath)
  416. }
  417. } else if soPrefix.Match([]byte(fileName)) {
  418. soPath := path.Join(rr.pluginDir, plugin2.PluginTypes[t], fileName)
  419. err = filex.UnzipTo(file, soPath)
  420. if err != nil {
  421. return filenames, "", err
  422. }
  423. filenames = append(filenames, soPath)
  424. revokeFiles = append(revokeFiles, soPath)
  425. _, version = parseName(fileName)
  426. } else if strings.HasPrefix(fileName, "etc/") {
  427. err = filex.UnzipTo(file, path.Join(rr.etcDir, plugin2.PluginTypes[t], strings.Replace(fileName, "etc", name, 1)))
  428. if err != nil {
  429. return filenames, "", err
  430. }
  431. } else { //unzip other files
  432. err = filex.UnzipTo(file, path.Join(tempPath, fileName))
  433. if err != nil {
  434. return filenames, "", err
  435. }
  436. }
  437. }
  438. if len(filenames) != expFiles {
  439. return filenames, version, fmt.Errorf("invalid zip file: so file or conf file is missing")
  440. } else if haveInstallFile {
  441. //run install script if there is
  442. spath := path.Join(tempPath, "install.sh")
  443. shellParas = append(shellParas, spath)
  444. if 1 != len(shellParas) {
  445. copy(shellParas[1:], shellParas[0:])
  446. shellParas[0] = spath
  447. }
  448. cmd := exec.Command("/bin/sh", shellParas...)
  449. var outb, errb bytes.Buffer
  450. cmd.Stdout = &outb
  451. cmd.Stderr = &errb
  452. err := cmd.Run()
  453. if err != nil {
  454. for _, f := range revokeFiles {
  455. os.RemoveAll(f)
  456. }
  457. conf.Log.Infof(`err:%v stdout:%s stderr:%s`, err, outb.String(), errb.String())
  458. return filenames, "", err
  459. } else {
  460. conf.Log.Infof(`run install script:%s`, outb.String())
  461. conf.Log.Infof("install %s plugin %s", plugin2.PluginTypes[t], name)
  462. }
  463. }
  464. return filenames, version, nil
  465. }
  466. // binder factory implementations
  467. func (rr *Manager) Source(name string) (api.Source, error) {
  468. nf, err := rr.loadRuntime(plugin2.SOURCE, name)
  469. if err != nil {
  470. return nil, err
  471. }
  472. if nf == nil {
  473. return nil, nil
  474. }
  475. switch t := nf.(type) {
  476. case api.Source:
  477. return t, nil
  478. case func() api.Source:
  479. return t(), nil
  480. default:
  481. return nil, fmt.Errorf("exported symbol %s is not type of api.Source or function that return api.Source", t)
  482. }
  483. }
  484. func (rr *Manager) Sink(name string) (api.Sink, error) {
  485. nf, err := rr.loadRuntime(plugin2.SINK, name)
  486. if err != nil {
  487. return nil, err
  488. }
  489. if nf == nil {
  490. return nil, nil
  491. }
  492. var s api.Sink
  493. switch t := nf.(type) {
  494. case api.Sink:
  495. s = t
  496. case func() api.Sink:
  497. s = t()
  498. default:
  499. return nil, fmt.Errorf("exported symbol %s is not type of api.Sink or function that return api.Sink", t)
  500. }
  501. return s, nil
  502. }
  503. func (rr *Manager) Function(name string) (api.Function, error) {
  504. nf, err := rr.loadRuntime(plugin2.FUNCTION, name)
  505. if err != nil {
  506. return nil, err
  507. }
  508. if nf == nil {
  509. return nil, nil
  510. }
  511. var s api.Function
  512. switch t := nf.(type) {
  513. case api.Function:
  514. s = t
  515. case func() api.Function:
  516. s = t()
  517. default:
  518. return nil, fmt.Errorf("exported symbol %s is not type of api.Function or function that return api.Function", t)
  519. }
  520. return s, nil
  521. }
  522. func (rr *Manager) HasFunctionSet(name string) bool {
  523. _, ok := rr.get(plugin2.FUNCTION, name)
  524. return ok
  525. }
  526. // If not found, return nil,nil; Other errors return nil, err
  527. func (rr *Manager) loadRuntime(t plugin2.PluginType, name string) (plugin.Symbol, error) {
  528. ut := ucFirst(name)
  529. ptype := plugin2.PluginTypes[t]
  530. key := ptype + "/" + name
  531. var nf plugin.Symbol
  532. rr.RLock()
  533. nf, ok := rr.runtime[key]
  534. rr.RUnlock()
  535. if !ok {
  536. mod, err := rr.getSoFilePath(t, name, false)
  537. if err != nil {
  538. conf.Log.Debugf(fmt.Sprintf("cannot find the native plugin in path: %v", err))
  539. return nil, nil
  540. }
  541. conf.Log.Debugf("Opening plugin %s", mod)
  542. plug, err := plugin.Open(mod)
  543. if err != nil {
  544. return nil, fmt.Errorf("cannot open %s: %v", mod, err)
  545. }
  546. conf.Log.Debugf("Successfully open plugin %s", mod)
  547. nf, err = plug.Lookup(ut)
  548. if err != nil {
  549. conf.Log.Debugf(fmt.Sprintf("cannot find symbol %s, please check if it is exported", name))
  550. return nil, nil
  551. }
  552. conf.Log.Debugf("Successfully look-up plugin %s", mod)
  553. rr.Lock()
  554. rr.runtime[key] = nf
  555. rr.Unlock()
  556. }
  557. return nf, nil
  558. }
  559. // Return the lowercase version of so name. It may be upper case in path.
  560. func (rr *Manager) getSoFilePath(t plugin2.PluginType, name string, isSoName bool) (string, error) {
  561. var (
  562. v string
  563. soname string
  564. ok bool
  565. )
  566. // We must identify plugin or symbol when deleting function plugin
  567. if isSoName {
  568. soname = name
  569. } else {
  570. soname, ok = rr.GetPluginBySymbol(t, name)
  571. if !ok {
  572. return "", errorx.NewWithCode(errorx.NOT_FOUND, fmt.Sprintf("invalid symbol name %s: not exist", name))
  573. }
  574. }
  575. v, ok = rr.get(t, soname)
  576. if !ok {
  577. return "", errorx.NewWithCode(errorx.NOT_FOUND, fmt.Sprintf("invalid name %s: not exist", soname))
  578. }
  579. soFile := soname + ".so"
  580. if v != "" {
  581. soFile = fmt.Sprintf("%s@%s.so", soname, v)
  582. }
  583. p := path.Join(rr.pluginDir, plugin2.PluginTypes[t], soFile)
  584. if _, err := os.Stat(p); err != nil {
  585. p = path.Join(rr.pluginDir, plugin2.PluginTypes[t], ucFirst(soFile))
  586. }
  587. if _, err := os.Stat(p); err != nil {
  588. return "", errorx.NewWithCode(errorx.NOT_FOUND, fmt.Sprintf("cannot find .so file for plugin %s", soname))
  589. }
  590. return p, nil
  591. }
  592. func parseName(n string) (string, string) {
  593. result := strings.Split(n, ".so")
  594. result = strings.Split(result[0], "@")
  595. name := lcFirst(result[0])
  596. if len(result) > 1 {
  597. return name, result[1]
  598. }
  599. return name, ""
  600. }
  601. func ucFirst(str string) string {
  602. for i, v := range str {
  603. return string(unicode.ToUpper(v)) + str[i+1:]
  604. }
  605. return ""
  606. }
  607. func lcFirst(str string) string {
  608. for i, v := range str {
  609. return string(unicode.ToLower(v)) + str[i+1:]
  610. }
  611. return ""
  612. }