Full Code of nightlyone/lockfile for AI

master bf01bef5587f cached
12 files
15.4 KB
4.6k tokens
23 symbols
1 requests
Download .txt
Repository: nightlyone/lockfile
Branch: master
Commit: bf01bef5587f
Files: 12
Total size: 15.4 KB

Directory structure:
gitextract_tczxkbkg/

├── .gitignore
├── .gitmodules
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── go.mod
├── lockfile.go
├── lockfile_test.go
├── lockfile_unix.go
├── lockfile_windows.go
└── renovate.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# popular temporaries
.err
.out
.diff

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe


================================================
FILE: .gitmodules
================================================
[submodule "git-hooks"]
	path = git-hooks
	url = https://github.com/nightlyone/git-hooks


================================================
FILE: .travis.yml
================================================
language: go
arch:
  - ppc64le
  - amd64
go:
  - 1.13
  - 1.14
  - tip

# Only test commits to production branch and all pull requests
branches:
  only:
    - master

matrix:
  allow_failures:
  - go: tip


================================================
FILE: LICENSE
================================================
Copyright (c) 2012 Ingo Oeser

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
lockfile
=========
Handle locking via pid files.

[![Build Status Unix][1]][2]
[![Build status Windows][3]][4]

[1]: https://secure.travis-ci.org/nightlyone/lockfile.png
[2]: https://travis-ci.org/nightlyone/lockfile
[3]: https://ci.appveyor.com/api/projects/status/7mojkmauj81uvp8u/branch/master?svg=true
[4]: https://ci.appveyor.com/project/nightlyone/lockfile/branch/master



install
-------
Install [Go 1][5], either [from source][6] or [with a prepackaged binary][7].
For Windows suport, Go 1.4 or newer is required.

Then run

	go get github.com/nightlyone/lockfile

[5]: http://golang.org
[6]: http://golang.org/doc/install/source
[7]: http://golang.org/doc/install

LICENSE
-------
MIT

documentation
-------------
[package documentation at godoc.org](http://godoc.org/github.com/nightlyone/lockfile)

install
-------------------
	go get github.com/nightlyone/lockfile


contributing
============

Contributions are welcome. Please open an issue or send me a pull request for a dedicated branch.
Make sure the git commit hooks show it works.

git commit hooks
-----------------------
enable commit hooks via

        cd .git ; rm -rf hooks; ln -s ../git-hooks hooks ; cd ..



================================================
FILE: appveyor.yml
================================================
clone_folder: c:\gopath\src\github.com\nightlyone\lockfile

environment:
  GOPATH: c:\gopath

install:
  - go version
  - go env
  - go get -v -t ./...

build_script:
  - go test -v ./...


================================================
FILE: go.mod
================================================
module github.com/nightlyone/lockfile

go 1.11


================================================
FILE: lockfile.go
================================================
// Package lockfile handles pid file based locking.
// While a sync.Mutex helps against concurrency issues within a single process,
// this package is designed to help against concurrency issues between cooperating processes
// or serializing multiple invocations of the same process. You can also combine sync.Mutex
// with Lockfile in order to serialize an action between different goroutines in a single program
// and also multiple invocations of this program.
package lockfile

import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
)

// Lockfile is a pid file which can be locked
type Lockfile string

// TemporaryError is a type of error where a retry after a random amount of sleep should help to mitigate it.
type TemporaryError string

func (t TemporaryError) Error() string { return string(t) }

// Temporary returns always true.
// It exists, so you can detect it via
//	if te, ok := err.(interface{ Temporary() bool }); ok {
//		fmt.Println("I am a temporary error situation, so wait and retry")
//	}
func (t TemporaryError) Temporary() bool { return true }

// Various errors returned by this package
var (
	ErrBusy          = TemporaryError("Locked by other process")             // If you get this, retry after a short sleep might help
	ErrNotExist      = TemporaryError("Lockfile created, but doesn't exist") // If you get this, retry after a short sleep might help
	ErrNeedAbsPath   = errors.New("Lockfiles must be given as absolute path names")
	ErrInvalidPid    = errors.New("Lockfile contains invalid pid for system")
	ErrDeadOwner     = errors.New("Lockfile contains pid of process not existent on this system anymore")
	ErrRogueDeletion = errors.New("Lockfile owned by me has been removed unexpectedly")
)

// New describes a new filename located at the given absolute path.
func New(path string) (Lockfile, error) {
	if !filepath.IsAbs(path) {
		return Lockfile(""), ErrNeedAbsPath
	}

	return Lockfile(path), nil
}

// GetOwner returns who owns the lockfile.
func (l Lockfile) GetOwner() (*os.Process, error) {
	name := string(l)

	// Ok, see, if we have a stale lockfile here
	content, err := ioutil.ReadFile(name)
	if err != nil {
		return nil, err
	}

	// try hard for pids. If no pid, the lockfile is junk anyway and we delete it.
	pid, err := scanPidLine(content)
	if err != nil {
		return nil, err
	}

	running, err := isRunning(pid)
	if err != nil {
		return nil, err
	}

	if running {
		proc, err := os.FindProcess(pid)
		if err != nil {
			return nil, err
		}

		return proc, nil
	}

	return nil, ErrDeadOwner
}

// TryLock tries to own the lock.
// It Returns nil, if successful and and error describing the reason, it didn't work out.
// Please note, that existing lockfiles containing pids of dead processes
// and lockfiles containing no pid at all are simply deleted.
func (l Lockfile) TryLock() error {
	name := string(l)

	// This has been checked by New already. If we trigger here,
	// the caller didn't use New and re-implemented it's functionality badly.
	// So panic, that he might find this easily during testing.
	if !filepath.IsAbs(name) {
		panic(ErrNeedAbsPath)
	}

	tmplock, cleanup, err := makePidFile(name, os.Getpid())
	if err != nil {
		return err
	}

	defer cleanup()

	// EEXIST and similar error codes, caught by os.IsExist, are intentionally ignored,
	// as it means that someone was faster creating this link
	// and ignoring this kind of error is part of the algorithm.
	// Then we will probably fail the pid owner check later, if this process is still alive.
	// We cannot ignore ALL errors, since failure to support hard links, disk full
	// as well as many other errors can happen to a filesystem operation
	// and we really want to abort on those.
	if err := os.Link(tmplock, name); err != nil {
		if !os.IsExist(err) {
			return err
		}
	}

	fiTmp, err := os.Lstat(tmplock)
	if err != nil {
		return err
	}

	fiLock, err := os.Lstat(name)
	if err != nil {
		// tell user that a retry would be a good idea
		if os.IsNotExist(err) {
			return ErrNotExist
		}

		return err
	}

	// Success
	if os.SameFile(fiTmp, fiLock) {
		return nil
	}

	proc, err := l.GetOwner()
	switch err {
	default:
		// Other errors -> defensively fail and let caller handle this
		return err
	case nil:
		if proc.Pid != os.Getpid() {
			return ErrBusy
		}
	case ErrDeadOwner, ErrInvalidPid: // cases we can fix below
	}

	// clean stale/invalid lockfile
	err = os.Remove(name)
	if err != nil {
		// If it doesn't exist, then it doesn't matter who removed it.
		if !os.IsNotExist(err) {
			return err
		}
	}

	// now that the stale lockfile is gone, let's recurse
	return l.TryLock()
}

// Unlock a lock again, if we owned it. Returns any error that happened during release of lock.
func (l Lockfile) Unlock() error {
	proc, err := l.GetOwner()
	switch err {
	case ErrInvalidPid, ErrDeadOwner:
		return ErrRogueDeletion
	case nil:
		if proc.Pid == os.Getpid() {
			// we really own it, so let's remove it.
			return os.Remove(string(l))
		}
		// Not owned by me, so don't delete it.
		return ErrRogueDeletion
	default:
		// This is an application error or system error.
		// So give a better error for logging here.
		if os.IsNotExist(err) {
			return ErrRogueDeletion
		}
		// Other errors -> defensively fail and let caller handle this
		return err
	}
}

func scanPidLine(content []byte) (int, error) {
	if len(content) == 0 {
		return 0, ErrInvalidPid
	}

	var pid int
	if _, err := fmt.Sscanln(string(content), &pid); err != nil {
		return 0, ErrInvalidPid
	}

	if pid <= 0 {
		return 0, ErrInvalidPid
	}

	return pid, nil
}

func makePidFile(name string, pid int) (tmpname string, cleanup func(), err error) {
	tmplock, err := ioutil.TempFile(filepath.Dir(name), filepath.Base(name)+".")
	if err != nil {
		return "", nil, err
	}

	cleanup = func() {
		_ = tmplock.Close()
		_ = os.Remove(tmplock.Name())
	}

	if _, err := io.WriteString(tmplock, fmt.Sprintf("%d\n", pid)); err != nil {
		cleanup() // Do cleanup here, so call doesn't have to.
		return "", nil, err
	}

	return tmplock.Name(), cleanup, nil
}


================================================
FILE: lockfile_test.go
================================================
package lockfile

import (
	"fmt"
	"io/ioutil"
	"math/rand"
	"os"
	"path/filepath"
	"strconv"
	"testing"
)

func ExampleLockfile() {
	lock, err := New(filepath.Join(os.TempDir(), "lock.me.now.lck"))
	if err != nil {
		fmt.Printf("Cannot init lock. reason: %v", err)
		panic(err) // handle properly please!
	}

	// Error handling is essential, as we only try to get the lock.
	if err = lock.TryLock(); err != nil {
		fmt.Printf("Cannot lock %q, reason: %v", lock, err)
		panic(err) // handle properly please!
	}

	defer func() {
		if err := lock.Unlock(); err != nil {
			fmt.Printf("Cannot unlock %q, reason: %v", lock, err)
			panic(err) // handle properly please!
		}
	}()

	fmt.Println("Do stuff under lock")
	// Output: Do stuff under lock
}

func TestBasicLockUnlock(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		panic(err)
	}

	lf, err := New(path)
	if err != nil {
		t.Fail()
		fmt.Println("Error making lockfile: ", err)
		return
	}

	err = lf.TryLock()
	if err != nil {
		t.Fail()
		fmt.Println("Error locking lockfile: ", err)
		return
	}

	err = lf.Unlock()
	if err != nil {
		t.Fail()
		fmt.Println("Error unlocking lockfile: ", err)
		return
	}
}

func GetDeadPID() int {
	// I have no idea how windows handles large PIDs, or if they even exist.
	// So limit it to be less or equal to 4096 to be safe.
	const maxPid = 4095

	// limited iteration, so we finish one day
	seen := map[int]bool{}
	for len(seen) < maxPid {
		pid := rand.Intn(maxPid + 1) // see https://godoc.org/math/rand#Intn why
		if seen[pid] {
			continue
		}

		seen[pid] = true

		running, err := isRunning(pid)
		if err != nil {
			fmt.Println("Error checking PID: ", err)
			continue
		}

		if !running {
			return pid
		}
	}
	panic(fmt.Sprintf("all pids lower %d are used, cannot test this", maxPid))
}

func TestBusy(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}

	pid := os.Getppid()

	if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}
	defer os.Remove(path)

	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}

	got := lf.TryLock()
	if got != ErrBusy {
		t.Fatalf("expected error %q, got %v", ErrBusy, got)
		return
	}
}

func TestRogueDeletion(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}
	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}
	err = lf.TryLock()
	if err != nil {
		t.Fatal(err)
		return
	}
	err = os.Remove(path)
	if err != nil {
		t.Fatal(err)
		return
	}

	got := lf.Unlock()
	if got != ErrRogueDeletion {
		t.Fatalf("unexpected error: %v", got)
		return
	}
}

func TestRogueDeletionDeadPid(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}
	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}
	err = lf.TryLock()
	if err != nil {
		t.Fatal(err)
		return
	}

	pid := GetDeadPID()
	if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}

	defer os.Remove(path)

	err = lf.Unlock()
	if err != ErrRogueDeletion {
		t.Fatalf("unexpected error: %v", err)
		return
	}

	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			content, _ := ioutil.ReadFile(path)
			t.Fatalf("lockfile %q (%q) should not be deleted by us, if we didn't create it", path, content)
		}
		t.Fatalf("unexpected error %v", err)
	}
}

func TestRemovesStaleLockOnDeadOwner(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}
	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}
	pid := GetDeadPID()
	if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}
	err = lf.TryLock()
	if err != nil {
		t.Fatal(err)
		return
	}

	if err := lf.Unlock(); err != nil {
		t.Fatal(err)
		return
	}
}

func TestInvalidPidLeadToReplacedLockfileAndSuccess(t *testing.T) {
	path, err := filepath.Abs("test_lockfile.pid")
	if err != nil {
		t.Fatal(err)
		return
	}

	if err := ioutil.WriteFile(path, []byte("\n"), 0666); err != nil {
		t.Fatal(err)
		return
	}

	defer os.Remove(path)

	lf, err := New(path)
	if err != nil {
		t.Fatal(err)
		return
	}

	if err := lf.TryLock(); err != nil {
		t.Fatalf("unexpected error: %v", err)
		return
	}

	// now check if file exists and contains the correct content
	got, err := ioutil.ReadFile(path)
	if err != nil {
		t.Fatalf("unexpected error %v", err)
		return
	}
	want := fmt.Sprintf("%d\n", os.Getpid())
	if string(got) != want {
		t.Fatalf("got %q, want %q", got, want)
	}
}

func TestScanPidLine(t *testing.T) {
	tests := [...]struct {
		input []byte
		pid   int
		xfail error
	}{
		{xfail: ErrInvalidPid},
		{input: []byte(""), xfail: ErrInvalidPid},
		{input: []byte("\n"), xfail: ErrInvalidPid},
		{input: []byte("-1\n"), xfail: ErrInvalidPid},
		{input: []byte("0\n"), xfail: ErrInvalidPid},
		{input: []byte("a\n"), xfail: ErrInvalidPid},
		{input: []byte("1\n"), pid: 1},
	}

	// test positive cases first
	for step, tc := range tests {
		if tc.xfail != nil {
			continue
		}

		got, err := scanPidLine(tc.input)
		if err != nil {
			t.Fatalf("%d: unexpected error %v", step, err)
		}

		if want := tc.pid; got != want {
			t.Errorf("%d: expected pid %d, got %d", step, want, got)
		}
	}

	// test negative cases now
	for step, tc := range tests {
		if tc.xfail == nil {
			continue
		}

		_, got := scanPidLine(tc.input)
		if want := tc.xfail; got != want {
			t.Errorf("%d: expected error %v, got %v", step, want, got)
		}
	}
}


================================================
FILE: lockfile_unix.go
================================================
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris aix

package lockfile

import (
	"os"
	"syscall"
)

func isRunning(pid int) (bool, error) {
	proc, err := os.FindProcess(pid)
	if err != nil {
		return false, err
	}

	if err := proc.Signal(syscall.Signal(0)); err != nil {
		return false, nil
	}

	return true, nil
}


================================================
FILE: lockfile_windows.go
================================================
package lockfile

import (
	"syscall"
)

//For some reason these consts don't exist in syscall.
const (
	error_invalid_parameter = 87
	code_still_active       = 259
)

func isRunning(pid int) (bool, error) {
	procHnd, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, true, uint32(pid))
	if err != nil {
		if scerr, ok := err.(syscall.Errno); ok {
			if uintptr(scerr) == error_invalid_parameter {
				return false, nil
			}
		}
	}

	var code uint32
	err = syscall.GetExitCodeProcess(procHnd, &code)
	if err != nil {
		return false, err
	}

	return code == code_still_active, nil
}


================================================
FILE: renovate.json
================================================
{
  "extends": [
    "config:base"
  ]
}
Download .txt
gitextract_tczxkbkg/

├── .gitignore
├── .gitmodules
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── go.mod
├── lockfile.go
├── lockfile_test.go
├── lockfile_unix.go
├── lockfile_windows.go
└── renovate.json
Download .txt
SYMBOL INDEX (23 symbols across 4 files)

FILE: lockfile.go
  type Lockfile (line 19) | type Lockfile
    method GetOwner (line 53) | func (l Lockfile) GetOwner() (*os.Process, error) {
    method TryLock (line 89) | func (l Lockfile) TryLock() error {
    method Unlock (line 165) | func (l Lockfile) Unlock() error {
  type TemporaryError (line 22) | type TemporaryError
    method Error (line 24) | func (t TemporaryError) Error() string { return string(t) }
    method Temporary (line 31) | func (t TemporaryError) Temporary() bool { return true }
  function New (line 44) | func New(path string) (Lockfile, error) {
  function scanPidLine (line 188) | func scanPidLine(content []byte) (int, error) {
  function makePidFile (line 205) | func makePidFile(name string, pid int) (tmpname string, cleanup func(), ...

FILE: lockfile_test.go
  function ExampleLockfile (line 13) | func ExampleLockfile() {
  function TestBasicLockUnlock (line 37) | func TestBasicLockUnlock(t *testing.T) {
  function GetDeadPID (line 65) | func GetDeadPID() int {
  function TestBusy (line 93) | func TestBusy(t *testing.T) {
  function TestRogueDeletion (line 121) | func TestRogueDeletion(t *testing.T) {
  function TestRogueDeletionDeadPid (line 150) | func TestRogueDeletionDeadPid(t *testing.T) {
  function TestRemovesStaleLockOnDeadOwner (line 190) | func TestRemovesStaleLockOnDeadOwner(t *testing.T) {
  function TestInvalidPidLeadToReplacedLockfileAndSuccess (line 218) | func TestInvalidPidLeadToReplacedLockfileAndSuccess(t *testing.T) {
  function TestScanPidLine (line 255) | func TestScanPidLine(t *testing.T) {

FILE: lockfile_unix.go
  function isRunning (line 10) | func isRunning(pid int) (bool, error) {

FILE: lockfile_windows.go
  constant error_invalid_parameter (line 9) | error_invalid_parameter = 87
  constant code_still_active (line 10) | code_still_active       = 259
  function isRunning (line 13) | func isRunning(pid int) (bool, error) {
Condensed preview — 12 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (18K chars).
[
  {
    "path": ".gitignore",
    "chars": 291,
    "preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# popular temporar"
  },
  {
    "path": ".gitmodules",
    "chars": 89,
    "preview": "[submodule \"git-hooks\"]\n\tpath = git-hooks\n\turl = https://github.com/nightlyone/git-hooks\n"
  },
  {
    "path": ".travis.yml",
    "chars": 205,
    "preview": "language: go\narch:\n  - ppc64le\n  - amd64\ngo:\n  - 1.13\n  - 1.14\n  - tip\n\n# Only test commits to production branch and all"
  },
  {
    "path": "LICENSE",
    "chars": 1054,
    "preview": "Copyright (c) 2012 Ingo Oeser\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this soft"
  },
  {
    "path": "README.md",
    "chars": 1184,
    "preview": "lockfile\n=========\nHandle locking via pid files.\n\n[![Build Status Unix][1]][2]\n[![Build status Windows][3]][4]\n\n[1]: htt"
  },
  {
    "path": "appveyor.yml",
    "chars": 188,
    "preview": "clone_folder: c:\\gopath\\src\\github.com\\nightlyone\\lockfile\n\nenvironment:\n  GOPATH: c:\\gopath\n\ninstall:\n  - go version\n  "
  },
  {
    "path": "go.mod",
    "chars": 47,
    "preview": "module github.com/nightlyone/lockfile\n\ngo 1.11\n"
  },
  {
    "path": "lockfile.go",
    "chars": 6068,
    "preview": "// Package lockfile handles pid file based locking.\n// While a sync.Mutex helps against concurrency issues within a sing"
  },
  {
    "path": "lockfile_test.go",
    "chars": 5661,
    "preview": "package lockfile\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc Example"
  },
  {
    "path": "lockfile_unix.go",
    "chars": 336,
    "preview": "// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris aix\n\npackage lockfile\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n"
  },
  {
    "path": "lockfile_windows.go",
    "chars": 594,
    "preview": "package lockfile\n\nimport (\n\t\"syscall\"\n)\n\n//For some reason these consts don't exist in syscall.\nconst (\n\terror_invalid_p"
  },
  {
    "path": "renovate.json",
    "chars": 41,
    "preview": "{\n  \"extends\": [\n    \"config:base\"\n  ]\n}\n"
  }
]

About this extraction

This page contains the full source code of the nightlyone/lockfile GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 12 files (15.4 KB), approximately 4.6k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!