Repository: juicedata/juicefs
Pull request #2197 (2022-06-09 15:00:08)

cmd/rmr: add progress bar for rmr


Patch:
diff --git a/cmd/rmr.go b/cmd/rmr.go
index 5769d66e..9062d7cb 100644
--- a/cmd/rmr.go
+++ b/cmd/rmr.go
@@ -21,7 +21,6 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
-	"syscall"
 
 	"github.com/juicedata/juicefs/pkg/meta"
 	"github.com/juicedata/juicefs/pkg/utils"
@@ -70,6 +69,8 @@ func rmr(ctx *cli.Context) error {
 		logger.Infof("Windows is not supported")
 		return nil
 	}
+	progress := utils.NewProgress(false, true)
+	spin := progress.AddCountSpinner("Removed entries")
 	for i := 0; i < ctx.Args().Len(); i++ {
 		path := ctx.Args().Get(i)
 		p, err := filepath.Abs(path)
@@ -98,16 +99,13 @@ func rmr(ctx *cli.Context) error {
 		if err != nil {
 			logger.Fatalf("write message: %s", err)
 		}
-		var errs = make([]byte, 1)
-		_ = readControl(f, errs)
-		if errs[0] != 0 {
-			errno := syscall.Errno(errs[0])
-			if runtime.GOOS == "windows" {
-				errno += 0x20000000
-			}
+		if errno := readProgress(f, func(count, bytes uint64) {
+			spin.SetCurrent(int64(count))
+		}); errno != 0 {
 			logger.Fatalf("RMR %s: %s", path, errno)
 		}
 		_ = f.Close()
 	}
+	progress.Done()
 	return nil
 }
diff --git a/cmd/warmup.go b/cmd/warmup.go
index 992a6fcb..83f7c634 100644
--- a/cmd/warmup.go
+++ b/cmd/warmup.go
@@ -18,11 +18,14 @@ package cmd
 
 import (
 	"bufio"
+	"encoding/binary"
 	"fmt"
 	"io"
 	"os"
 	"path/filepath"
+	"runtime"
 	"strings"
+	"syscall"
 	"time"
 
 	"github.com/juicedata/juicefs/pkg/meta"
@@ -86,8 +89,32 @@ func readControl(cf *os.File, resp []byte) int {
 	}
 }
 
+func readProgress(cf *os.File, showProgress func(uint64, uint64)) (errno syscall.Errno) {
+	var resp = make([]byte, 1024)
+END:
+	for {
+		n := readControl(cf, resp)
+		for off := 0; off < n; {
+			if off+1 == n {
+				errno = syscall.Errno(resp[off])
+				break END
+			} else if off+17 <= n && resp[off] == meta.CPROGRESS {
+				showProgress(binary.BigEndian.Uint64(resp[off+1:off+9]), binary.BigEndian.Uint64(resp[off+9:off+17]))
+				off += 17
+			} else {
+				logger.Errorf("Bad response off %d n %d: %v", off, n, resp)
+				break
+			}
+		}
+	}
+	if errno != 0 && runtime.GOOS == "windows" {
+		errno += 0x20000000
+	}
+	return
+}
+
 // send fill-cache command to controller file
-func sendCommand(cf *os.File, batch []string, threads uint, background bool) {
+func sendCommand(cf *os.File, batch []string, threads uint, background bool, dspin *utils.DoubleSpinner) {
 	paths := strings.Join(batch, "\n")
 	var back uint8
 	if background {
@@ -107,10 +134,10 @@ func sendCommand(cf *os.File, batch []string, threads uint, background bool) {
 		logger.Infof("Warm-up cache for %d paths in background", len(batch))
 		return
 	}
-	var errs = make([]byte, 1)
-	_ = readControl(cf, errs) // 0 < n <= 1
-	if errs[0] != 0 {
-		logger.Fatalf("Warm up failed: %d", errs[0])
+	if errno := readProgress(cf, func(count, bytes uint64) {
+		dspin.SetCurrent(int64(count), int64(bytes))
+	}); errno != 0 {
+		logger.Fatalf("Warm up failed: %s", errno)
 	}
 }
 
@@ -176,8 +203,8 @@ func warmup(ctx *cli.Context) error {
 	background := ctx.Bool("background")
 	start := len(mp)
 	batch := make([]string, 0, batchMax)
-	progress := utils.NewProgress(background, false)
-	bar := progress.AddCountBar("Warmed up paths", int64(len(paths)))
+	progress := utils.NewProgress(background, true)
+	dspin := progress.AddDoubleSpinner("Warmed up")
 	for _, path := range paths {
 		if mp == "/" {
 			inode, err := utils.GetFileInode(path)
@@ -193,18 +220,17 @@ func warmup(ctx *cli.Context) error {
 			continue
 		}
 		if len(batch) >= batchMax {
-			sendCommand(controller, batch, threads, background)
-			bar.IncrBy(len(batch))
+			sendCommand(controller, batch, threads, background, dspin)
 			batch = batch[0:]
 		}
 	}
 	if len(batch) > 0 {
-		sendCommand(controller, batch, threads, background)
-		bar.IncrBy(len(batch))
+		sendCommand(controller, batch, threads, background, dspin)
 	}
 	progress.Done()
 	if !background {
-		logger.Infof("Successfully warmed up %d paths", bar.Current())
+		count, bytes := dspin.Current()
+		logger.Infof("Successfully warmed up %d files (%d bytes)", count, bytes)
 	}
 
 	return nil
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index 8f972aa3..ad5dc7b0 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -438,7 +438,7 @@ func (fs *FileSystem) Rmr(ctx meta.Context, p string) (err syscall.Errno) {
 	if err != 0 {
 		return
 	}
-	err = meta.Remove(fs.m, ctx, parent.inode, path.Base(p))
+	err = meta.Remove(fs.m, ctx, parent.inode, path.Base(p), nil)
 	fs.invalidateEntry(parent.inode, path.Base(p))
 	return
 }
diff --git a/pkg/meta/interface.go b/pkg/meta/interface.go
index 5d2e892a..72154eb6 100644
--- a/pkg/meta/interface.go
+++ b/pkg/meta/interface.go
@@ -87,6 +87,9 @@ type internalNode struct {
 	name  string
 }
 
+// Type of control messages
+const CPROGRESS = 0xFE // 16 bytes: progress increment
+
 // MsgCallback is a callback for messages from meta service.
 type MsgCallback func(...interface{}) error
 
diff --git a/pkg/meta/utils.go b/pkg/meta/utils.go
index efd059d8..86091aa1 100644
--- a/pkg/meta/utils.go
+++ b/pkg/meta/utils.go
@@ -24,6 +24,7 @@ import (
 	"sort"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"syscall"
 	"time"
 
@@ -221,7 +222,7 @@ func updateLocks(ls []plockRecord, nl plockRecord) []plockRecord {
 	return ls
 }
 
-func emptyDir(r Meta, ctx Context, inode Ino, concurrent chan int) syscall.Errno {
+func emptyDir(r Meta, ctx Context, inode Ino, count *uint64, concurrent chan int) syscall.Errno {
 	if st := r.Access(ctx, inode, 3, nil); st != 0 {
 		return st
 	}
@@ -241,19 +242,23 @@ func emptyDir(r Meta, ctx Context, inode Ino, concurrent chan int) syscall.Errno
 				wg.Add(1)
 				go func(child Ino, name string) {
 					defer wg.Done()
-					e := emptyEntry(r, ctx, inode, name, child, concurrent)
+					e := emptyEntry(r, ctx, inode, name, child, count, concurrent)
 					if e != 0 {
 						status = e
 					}
 					<-concurrent
 				}(e.Inode, string(e.Name))
 			default:
-				if st := emptyEntry(r, ctx, inode, string(e.Name), e.Inode, concurrent); st != 0 {
+				if st := emptyEntry(r, ctx, inode, string(e.Name), e.Inode, count, concurrent); st != 0 {
 					return st
 				}
 			}
 		} else {
-			if st := r.Unlink(ctx, inode, string(e.Name)); st != 0 {
+			if st := r.Unlink(ctx, inode, string(e.Name)); st == 0 {
+				if count != nil {
+					atomic.AddUint64(count, 1)
+				}
+			} else {
 				return st
 			}
 		}
@@ -262,18 +267,22 @@ func emptyDir(r Meta, ctx Context, inode Ino, concurrent chan int) syscall.Errno
 	return status
 }
 
-func emptyEntry(r Meta, ctx Context, parent Ino, name string, inode Ino, concurrent chan int) syscall.Errno {
-	st := emptyDir(r, ctx, inode, concurrent)
+func emptyEntry(r Meta, ctx Context, parent Ino, name string, inode Ino, count *uint64, concurrent chan int) syscall.Errno {
+	st := emptyDir(r, ctx, inode, count, concurrent)
 	if st == 0 {
 		st = r.Rmdir(ctx, parent, name)
-		if st == syscall.ENOTEMPTY {
-			st = emptyEntry(r, ctx, parent, name, inode, concurrent)
+		if st == 0 {
+			if count != nil {
+				atomic.AddUint64(count, 1)
+			}
+		} else if st == syscall.ENOTEMPTY {
+			st = emptyEntry(r, ctx, parent, name, inode, count, concurrent)
 		}
 	}
 	return st
 }
 
-func Remove(r Meta, ctx Context, parent Ino, name string) syscall.Errno {
+func Remove(r Meta, ctx Context, parent Ino, name string, count *uint64) syscall.Errno {
 	if st := r.Access(ctx, parent, 3, nil); st != 0 {
 		return st
 	}
@@ -283,10 +292,14 @@ func Remove(r Meta, ctx Context, parent Ino, name string) syscall.Errno {
 		return st
 	}
 	if attr.Typ != TypeDirectory {
-		return r.Unlink(ctx, parent, name)
+		st := r.Unlink(ctx, parent, name)
+		if st == 0 && count != nil {
+			atomic.AddUint64(count, 1)
+		}
+		return st
 	}
 	concurrent := make(chan int, 50)
-	return emptyEntry(r, ctx, parent, name, inode, concurrent)
+	return emptyEntry(r, ctx, parent, name, inode, count, concurrent)
 }
 
 func GetSummary(r Meta, ctx Context, inode Ino, summary *Summary, recursive bool) syscall.Errno {
diff --git a/pkg/utils/progress.go b/pkg/utils/progress.go
index 68384861..71076589 100644
--- a/pkg/utils/progress.go
+++ b/pkg/utils/progress.go
@@ -69,6 +69,11 @@ func (s *DoubleSpinner) Current() (int64, int64) {
 	return s.count.Current(), s.bytes.Current()
 }
 
+func (s *DoubleSpinner) SetCurrent(count, bytes int64) {
+	s.count.SetCurrent(count)
+	s.bytes.SetCurrent(bytes)
+}
+
 func NewProgress(quiet, showSpeed bool) *Progress {
 	var p *Progress
 	if quiet || os.Getenv("DISPLAY_PROGRESSBAR") == "false" || !isatty.IsTerminal(os.Stdout.Fd()) {
@@ -107,15 +112,15 @@ func newSpinner() mpb.BarFiller {
 }
 
 func (p *Progress) AddCountSpinner(name string) *Bar {
-	placeholders := []decor.WC{decor.WCSyncSpaceR}
-	if p.showSpeed { // no real speed; just add an empty placeholder for now
-		placeholders = append(placeholders, decor.WCSyncSpaceR)
+	decors := []decor.Decorator{
+		decor.Name(name+" count: ", decor.WCSyncWidth),
+		decor.Merge(decor.CurrentNoUnit("%d", decor.WCSyncSpaceR), decor.WCSyncSpaceR),
+	}
+	if p.showSpeed {
+		decors = append(decors, decor.AverageSpeed(0, "  %.2f/s", decor.WCSyncSpaceR))
 	}
 	b := p.Progress.Add(0, newSpinner(),
-		mpb.PrependDecorators(
-			decor.Name(name+" count: ", decor.WCSyncWidth),
-			decor.Merge(decor.CurrentNoUnit("%d", decor.WCSyncSpaceR), placeholders...),
-		),
+		mpb.PrependDecorators(decors...),
 		mpb.BarFillerClearOnComplete(),
 	)
 	p.bars = append(p.bars, b)
diff --git a/pkg/vfs/fill.go b/pkg/vfs/fill.go
index ce59ca4d..d89644a9 100644
--- a/pkg/vfs/fill.go
+++ b/pkg/vfs/fill.go
@@ -22,6 +22,7 @@ import (
 	"strconv"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"syscall"
 	"time"
 
@@ -33,7 +34,7 @@ type _file struct {
 	size uint64
 }
 
-func (v *VFS) fillCache(paths []string, concurrent int) {
+func (v *VFS) fillCache(paths []string, concurrent int, count, bytes *uint64) {
 	logger.Infof("start to warmup %d paths with %d workers", len(paths), concurrent)
 	start := time.Now()
 	todo := make(chan _file, 10240)
@@ -46,8 +47,14 @@ func (v *VFS) fillCache(paths []string, concurrent int) {
 				if f.ino == 0 {
 					break
 				}
-				err := v.fillInode(f.ino, f.size)
-				if err != nil { // TODO: print path instead of inode
+				if err := v.fillInode(f.ino, f.size); err == nil {
+					if count != nil {
+						atomic.AddUint64(count, 1)
+					}
+					if bytes != nil {
+						atomic.AddUint64(bytes, f.size)
+					}
+				} else { // TODO: print path instead of inode
 					logger.Errorf("Inode %d could be corrupted: %s", f.ino, err)
 				}
 			}
diff --git a/pkg/vfs/internal.go b/pkg/vfs/internal.go
index e6677f17..69f9d34f 100644
--- a/pkg/vfs/internal.go
+++ b/pkg/vfs/internal.go
@@ -22,6 +22,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"sync/atomic"
 	"syscall"
 	"time"
 
@@ -156,13 +157,46 @@ func collectMetrics(registry *prometheus.Registry) []byte {
 	return w.Bytes()
 }
 
-func (v *VFS) handleInternalMsg(ctx Context, cmd uint32, r *utils.Buffer) []byte {
+func writeProgress(count, bytes *uint64, data *[]byte, done chan struct{}) {
+	wb := utils.NewBuffer(17)
+	wb.Put8(meta.CPROGRESS)
+	if bytes == nil {
+		bytes = new(uint64)
+	}
+	ticker := time.NewTicker(time.Millisecond * 300)
+	for {
+		select {
+		case <-ticker.C:
+			wb.Put64(atomic.LoadUint64(count))
+			wb.Put64(atomic.LoadUint64(bytes))
+			*data = append(*data, wb.Bytes()...)
+			wb.Seek(1)
+		case <-done:
+			ticker.Stop()
+			if *count > 0 || *bytes > 0 {
+				wb.Put64(atomic.LoadUint64(count))
+				wb.Put64(atomic.LoadUint64(bytes))
+				*data = append(*data, wb.Bytes()...)
+			}
+			return
+		}
+	}
+}
+
+func (v *VFS) handleInternalMsg(ctx Context, cmd uint32, r *utils.Buffer, data *[]byte) {
 	switch cmd {
 	case meta.Rmr:
-		inode := Ino(r.Get64())
-		name := string(r.Get(int(r.Get8())))
-		r := meta.Remove(v.Meta, ctx, inode, name)
-		return []byte{uint8(r)}
+		done := make(chan struct{})
+		var count uint64
+		var st syscall.Errno
+		go func() {
+			inode := Ino(r.Get64())
+			name := string(r.Get(int(r.Get8())))
+			st = meta.Remove(v.Meta, ctx, inode, name, &count)
+			close(done)
+		}()
+		writeProgress(&count, nil, data, done)
+		*data = append(*data, uint8(st))
 	case meta.Info:
 		var summary meta.Summary
 		inode := Ino(r.Get64())
@@ -176,7 +210,8 @@ func (v *VFS) handleInternalMsg(ctx Context, cmd uint32, r *utils.Buffer) []byte
 		if r != 0 {
 			msg := r.Error()
 			wb.Put32(uint32(len(msg)))
-			return append(wb.Bytes(), []byte(msg)...)
+			*data = append(*data, append(wb.Bytes(), msg...)...)
+			return
 		}
 		var w = bytes.NewBuffer(nil)
 		fmt.Fprintf(w, " inode: %d\n", inode)
@@ -208,19 +243,25 @@ func (v *VFS) handleInternalMsg(ctx Context, cmd uint32, r *utils.Buffer) []byte
 			}
 		}
 		wb.Put32(uint32(w.Len()))
-		return append(wb.Bytes(), w.Bytes()...)
+		*data = append(*data, append(wb.Bytes(), w.Bytes()...)...)
 	case meta.FillCache:
 		paths := strings.Split(string(r.Get(int(r.Get32()))), "\n")
 		concurrent := r.Get16()
 		background := r.Get8()
 		if background == 0 {
-			v.fillCache(paths, int(concurrent))
+			var count, bytes uint64
+			done := make(chan struct{})
+			go func() {
+				v.fillCache(paths, int(concurrent), &count, &bytes)
+				close(done)
+			}()
+			writeProgress(&count, &bytes, data, done)
 		} else {
-			go v.fillCache(paths, int(concurrent))
+			go v.fillCache(paths, int(concurrent), nil, nil)
 		}
-		return []byte{uint8(0)}
+		*data = append(*data, uint8(0))
 	default:
 		logger.Warnf("unknown message type: %d", cmd)
-		return []byte{uint8(syscall.EINVAL & 0xff)}
+		*data = append(*data, uint8(syscall.EINVAL&0xff))
 	}
 }
diff --git a/pkg/vfs/vfs.go b/pkg/vfs/vfs.go
index 6c900bc2..6d8ccfa0 100644
--- a/pkg/vfs/vfs.go
+++ b/pkg/vfs/vfs.go
@@ -573,9 +573,7 @@ func (v *VFS) Write(ctx Context, ino Ino, buf []byte, off, fh uint64) (err sysca
 		h.data = append(h.data, h.pending...)
 		h.pending = h.pending[:0]
 		if rb.Left() == size {
-			go func() {
-				h.data = append(h.data, v.handleInternalMsg(ctx, cmd, rb)...)
-			}()
+			go v.handleInternalMsg(ctx, cmd, rb, &h.data)
 		} else {
 			logger.Warnf("broken message: %d %d < %d", cmd, size, rb.Left())
 			h.data = append(h.data, uint8(syscall.EIO&0xff))
diff --git a/sdk/java/Makefile b/sdk/java/Makefile
index bb48f092..f00e9f57 100644
--- a/sdk/java/Makefile
+++ b/sdk/java/Makefile
@@ -28,7 +28,7 @@ libjfs-all: libjfs.so
 		-w /go/src/github.com/juicedata/juicefs/sdk/java/libjfs \
 		--entrypoint=/bin/bash \
 		juicedata/golang-cross:latest \
-		-c 'make mac win'
+		-c 'make mac win linux-arm64 mac-arm64'
 
 libjfs.so:
 	docker run --rm \
diff --git a/sdk/java/libjfs/Makefile b/sdk/java/libjfs/Makefile
index 7a3013bb..63965677 100644
--- a/sdk/java/libjfs/Makefile
+++ b/sdk/java/libjfs/Makefile
@@ -9,24 +9,27 @@ LDFLAGS += -X $(PKG).revision=$(REVISION) \
 		-X $(PKG).revisionDate=$(REVISIONDATE)
 GOROOT=$(shell go env GOROOT)
 
-LIBFILE := libjfs.so
-OSNAME := linux
-
 ifeq ($(OS),Windows_NT)
     uname_S := Windows
 else
     uname_S := $(shell uname -s)
+    uname_m := $(shell uname -m)
+endif
+
+ARCHNAME := amd64
+
+ifeq ($(uname_m), aarch64)
+    ARCHNAME = arm64
 endif
 
+LIBFILE := libjfs-$(ARCHNAME).so
 ifeq ($(uname_S), Windows)
-    LIBFILE = libjfs.dll
-    OSNAME = windows
+    LIBFILE = libjfs-$(ARCHNAME).dll
     CC = /usr/bin/musl-gcc
     export CC
 endif
 ifeq ($(uname_S), Darwin)
-    LIBFILE = libjfs.dylib
-    OSNAME = darwin
+    LIBFILE = libjfs-$(ARCHNAME).dylib
 endif
 
 all: default
@@ -38,18 +41,33 @@ default: libjfs
 libjfs: *.go ../../../pkg/*/*.go
 	go build -buildmode=c-shared -ldflags="$(LDFLAGS)" -o $(LIBFILE) .
 
+linux-arm64: libjfs-arm64.so
+	mkdir -p target
+	gzip -c libjfs-arm64.so > target/libjfs-arm64.so.gz
+
+libjfs-arm64.so: *.go ../../../pkg/*/*.go
+	GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc go build -buildmode=c-shared -ldflags="$(LDFLAGS)" -o libjfs-arm64.so .
+
 mac: libjfs.dylib
 	mkdir -p target
-	gzip -c libjfs.dylib > target/libjfs.dylib.gz
+	gzip -c libjfs-amd64.dylib > target/libjfs-amd64.dylib.gz
 
 libjfs.dylib: *.go ../../../pkg/*/*.go
-	GOOS=darwin CGO_ENABLED=1 CC=o64-clang go build -o libjfs.dylib \
+	GOOS=darwin CGO_ENABLED=1 CC=o64-clang go build -o libjfs-amd64.dylib \
+	-buildmode=c-shared -ldflags="$(LDFLAGS)"
+
+mac-arm64: libjfs-arm64.dylib
+	mkdir -p target
+	gzip -c libjfs-arm64.dylib > target/libjfs-arm64.dylib.gz
+
+libjfs-arm64.dylib: *.go ../../../pkg/*/*.go
+	GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 CC=o64-clang go build -o libjfs-arm64.dylib \
 	-buildmode=c-shared -ldflags="$(LDFLAGS)"
 
 win: libjfs.dll
 	mkdir -p target
-	gzip -c libjfs.dll > target/libjfs.dll.gz
+	gzip -c libjfs-amd64.dll > target/libjfs-amd64.dll.gz
 
 libjfs.dll: *.go ../../../pkg/*/*.go
-	GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o libjfs.dll \
+	GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o libjfs-amd64.dll \
 	-buildmode=c-shared -ldflags="$(LDFLAGS)"
diff --git a/sdk/java/pom.xml b/sdk/java/pom.xml
index a26cdc3e..8bd1ca76 100644
--- a/sdk/java/pom.xml
+++ b/sdk/java/pom.xml
@@ -112,7 +112,7 @@
 		<dependency>
 			<groupId>com.github.jnr</groupId>
 			<artifactId>jnr-ffi</artifactId>
-			<version>2.1.11</version>
+			<version>2.2.12</version>
 		</dependency>
 		<dependency>
 			<groupId>org.apache.hadoop</groupId>
diff --git a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
index 6b27b086..3e3e6f44 100644
--- a/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
+++ b/sdk/java/src/main/java/io/juicefs/JuiceFileSystemImpl.java
@@ -504,18 +504,29 @@ public class JuiceFileSystemImpl extends FileSystem {
 
     LibraryLoader<Libjfs> libjfsLibraryLoader = LibraryLoader.create(Libjfs.class);
     libjfsLibraryLoader.failImmediately();
-    String resource = "libjfs.so.gz";
-    String name = "libjfs.5.so";
+
+    int soVer = 6;
+    String osId = "so";
+    String archId = "amd64";
+    String resourceFormat = "libjfs-%s.%s.gz";
+    String nameFormat = "libjfs-%s.%d.%s";
+
     File dir = new File("/tmp");
     String os = System.getProperty("os.name");
+    String arch = System.getProperty("os.arch");
+    if (arch.contains("aarch64")) {
+      archId = "arm64";
+    }
     if (os.toLowerCase().contains("windows")) {
-      resource = "libjfs.dll.gz";
-      name = "libjfs5.dll";
+      osId = "dll";
       dir = new File(System.getProperty("java.io.tmpdir"));
     } else if (os.toLowerCase().contains("mac")) {
-      resource = "libjfs.dylib.gz";
-      name = "libjfs5.dylib";
+      osId = "dylib";
     }
+
+    String resource = String.format(resourceFormat, archId, osId);
+    String name = String.format(nameFormat, archId, soVer, osId);
+
     File libFile = new File(dir, name);
 
     URL res = null;


Test patch:
diff --git a/pkg/meta/base_test.go b/pkg/meta/base_test.go
index 36836e65..53f90e09 100644
--- a/pkg/meta/base_test.go
+++ b/pkg/meta/base_test.go
@@ -737,7 +737,7 @@ func testRemove(t *testing.T, m Meta) {
 	if st := m.Create(ctx, 1, "f", 0644, 0, 0, &inode, attr); st != 0 {
 		t.Fatalf("create f: %s", st)
 	}
-	if st := Remove(m, ctx, 1, "f"); st != 0 {
+	if st := Remove(m, ctx, 1, "f", nil); st != 0 {
 		t.Fatalf("rmr f: %s", st)
 	}
 	if st := m.Mkdir(ctx, 1, "d", 0755, 0, 0, &parent, attr); st != 0 {
@@ -766,7 +766,7 @@ func testRemove(t *testing.T, m Meta) {
 	} else if len(entries) != 4099 {
 		t.Fatalf("entries: %d", len(entries))
 	}
-	if st := Remove(m, ctx, 1, "d"); st != 0 {
+	if st := Remove(m, ctx, 1, "d", nil); st != 0 {
 		t.Fatalf("rmr d: %s", st)
 	}
 }
diff --git a/pkg/meta/benchmarks_test.go b/pkg/meta/benchmarks_test.go
index 11027a98..98d60db5 100644
--- a/pkg/meta/benchmarks_test.go
+++ b/pkg/meta/benchmarks_test.go
@@ -116,7 +116,7 @@ func BenchmarkReadSliceBuf(b *testing.B) {
 
 func prepareParent(m Meta, name string, inode *Ino) error {
 	ctx := Background
-	if err := Remove(m, ctx, 1, name); err != 0 && err != syscall.ENOENT {
+	if err := Remove(m, ctx, 1, name, nil); err != 0 && err != syscall.ENOENT {
 		return fmt.Errorf("remove: %s", err)
 	}
 	if err := m.Mkdir(ctx, 1, name, 0755, 0, 0, inode, nil); err != 0 {
diff --git a/pkg/vfs/fill_test.go b/pkg/vfs/fill_test.go
index a0212d5c..4c1ecf29 100644
--- a/pkg/vfs/fill_test.go
+++ b/pkg/vfs/fill_test.go
@@ -36,7 +36,7 @@ func TestFill(t *testing.T) {
 	_, _ = v.Symlink(ctx, "testfile", 1, "sym3")
 
 	// normal cases
-	v.fillCache([]string{"/test/file", "/test", "/sym", "/"}, 2)
+	v.fillCache([]string{"/test/file", "/test", "/sym", "/"}, 2, nil, nil)
 
 	// remove chunk
 	var slices []meta.Slice
@@ -45,5 +45,5 @@ func TestFill(t *testing.T) {
 		_ = v.Store.Remove(s.Chunkid, int(s.Size))
 	}
 	// bad cases
-	v.fillCache([]string{"/test/file", "/sym2", "/sym3", "/.stats", "/not_exists"}, 2)
+	v.fillCache([]string{"/test/file", "/sym2", "/sym3", "/.stats", "/not_exists"}, 2, nil, nil)
 }
diff --git a/pkg/vfs/vfs_test.go b/pkg/vfs/vfs_test.go
index 4130017f..9c3729bf 100644
--- a/pkg/vfs/vfs_test.go
+++ b/pkg/vfs/vfs_test.go
@@ -700,10 +700,17 @@ func TestInternalFile(t *testing.T) {
 	if e != 0 {
 		t.Fatalf("open .stats: %s", e)
 	}
-	readControl := func(resp []byte, off uint64) (int, syscall.Errno) {
+	readControl := func(resp []byte, off *uint64) (int, syscall.Errno) {
 		for {
-			if n, errno := v.Read(ctx, fe.Inode, resp, off, fh); n == 0 {
+			if n, errno := v.Read(ctx, fe.Inode, resp, *off, fh); n == 0 {
 				time.Sleep(time.Millisecond * 300)
+			} else if n%17 == 0 {
+				*off += uint64(n)
+				continue
+			} else if n%17 == 1 {
+				*off += uint64(n / 17 * 17)
+				resp[0] = resp[n-1]
+				return 1, errno
 			} else {
 				return n, errno
 			}
@@ -723,7 +730,7 @@ func TestInternalFile(t *testing.T) {
 	}
 	var off uint64 = uint64(len(buf))
 	resp := make([]byte, 1024*10)
-	if n, e := readControl(resp, off); e != 0 || n != 1 {
+	if n, e := readControl(resp, &off); e != 0 || n != 1 {
 		t.Fatalf("read result: %s %d", e, n)
 	} else if resp[0] != byte(syscall.ENOENT) {
 		t.Fatalf("rmr result: %s", string(buf[:n]))
@@ -741,8 +748,8 @@ func TestInternalFile(t *testing.T) {
 	}
 	off += uint64(len(buf))
 	buf = make([]byte, 1024*10)
-	if n, e = readControl(buf, off); e != 0 {
-		t.Fatalf("read result: %s", e)
+	if n, e = readControl(buf, &off); e != 0 {
+		t.Fatalf("read result: %s %d", e, n)
 	} else if !strings.Contains(string(buf[:n]), "dirs:") {
 		t.Fatalf("info result: %s", string(buf[:n]))
 	} else {
@@ -766,8 +773,8 @@ func TestInternalFile(t *testing.T) {
 	}
 	off += uint64(len(buf))
 	resp = make([]byte, 1024*10)
-	if n, e = readControl(resp, off); e != 0 || n != 1 {
-		t.Fatalf("read result: %s", e)
+	if n, e = readControl(resp, &off); e != 0 || n != 1 {
+		t.Fatalf("read result: %s %d", e, n)
 	} else if resp[0] != 0 {
 		t.Fatalf("fill result: %s", string(buf[:n]))
 	}