Imported Upstream version 0.0~git20141017.0.76626ae
Anthony Fok
8 years ago
0 | Copyright 2014 Alan Shreve | |
1 | ||
2 | Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | you may not use this file except in compliance with the License. | |
4 | You may obtain a copy of the License at | |
5 | ||
6 | http://www.apache.org/licenses/LICENSE-2.0 | |
7 | ||
8 | Unless required by applicable law or agreed to in writing, software | |
9 | distributed under the License is distributed on an "AS IS" BASIS, | |
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | See the License for the specific language governing permissions and | |
12 | limitations under the License. |
0 | # mousetrap | |
1 | ||
2 | mousetrap is a tiny library that answers a single question. | |
3 | ||
4 | On a Windows machine, was the process invoked by someone double clicking on | |
5 | the executable file while browsing in explorer? | |
6 | ||
7 | ### Motivation | |
8 | ||
9 | Windows developers unfamiliar with command line tools will often "double-click" | |
10 | the executable for a tool. Because most CLI tools print the help and then exit | |
11 | when invoked without arguments, this is often very frustrating for those users. | |
12 | ||
13 | mousetrap provides a way to detect these invocations so that you can provide | |
14 | more helpful behavior and instructions on how to run the CLI tool. To see what | |
15 | this looks like, both from an organizational and a technical perspective, see | |
16 | https://inconshreveable.com/09-09-2014/sweat-the-small-stuff/ | |
17 | ||
18 | ### The interface | |
19 | ||
20 | The library exposes a single interface: | |
21 | ||
22 | func StartedByExplorer() (bool) |
0 | // +build !windows | |
1 | ||
2 | package mousetrap | |
3 | ||
4 | // StartedByExplorer returns true if the program was invoked by the user | |
5 | // double-clicking on the executable from explorer.exe | |
6 | // | |
7 | // It is conservative and returns false if any of the internal calls fail. | |
8 | // It does not guarantee that the program was run from a terminal. It only can tell you | |
9 | // whether it was launched from explorer.exe | |
10 | // | |
11 | // On non-Windows platforms, it always returns false. | |
12 | func StartedByExplorer() bool { | |
13 | return false | |
14 | } |
0 | // +build windows | |
1 | // +build !go1.4 | |
2 | ||
3 | package mousetrap | |
4 | ||
5 | import ( | |
6 | "fmt" | |
7 | "os" | |
8 | "syscall" | |
9 | "unsafe" | |
10 | ) | |
11 | ||
12 | const ( | |
13 | // defined by the Win32 API | |
14 | th32cs_snapprocess uintptr = 0x2 | |
15 | ) | |
16 | ||
17 | var ( | |
18 | kernel = syscall.MustLoadDLL("kernel32.dll") | |
19 | CreateToolhelp32Snapshot = kernel.MustFindProc("CreateToolhelp32Snapshot") | |
20 | Process32First = kernel.MustFindProc("Process32FirstW") | |
21 | Process32Next = kernel.MustFindProc("Process32NextW") | |
22 | ) | |
23 | ||
24 | // ProcessEntry32 structure defined by the Win32 API | |
25 | type processEntry32 struct { | |
26 | dwSize uint32 | |
27 | cntUsage uint32 | |
28 | th32ProcessID uint32 | |
29 | th32DefaultHeapID int | |
30 | th32ModuleID uint32 | |
31 | cntThreads uint32 | |
32 | th32ParentProcessID uint32 | |
33 | pcPriClassBase int32 | |
34 | dwFlags uint32 | |
35 | szExeFile [syscall.MAX_PATH]uint16 | |
36 | } | |
37 | ||
38 | func getProcessEntry(pid int) (pe *processEntry32, err error) { | |
39 | snapshot, _, e1 := CreateToolhelp32Snapshot.Call(th32cs_snapprocess, uintptr(0)) | |
40 | if snapshot == uintptr(syscall.InvalidHandle) { | |
41 | err = fmt.Errorf("CreateToolhelp32Snapshot: %v", e1) | |
42 | return | |
43 | } | |
44 | defer syscall.CloseHandle(syscall.Handle(snapshot)) | |
45 | ||
46 | var processEntry processEntry32 | |
47 | processEntry.dwSize = uint32(unsafe.Sizeof(processEntry)) | |
48 | ok, _, e1 := Process32First.Call(snapshot, uintptr(unsafe.Pointer(&processEntry))) | |
49 | if ok == 0 { | |
50 | err = fmt.Errorf("Process32First: %v", e1) | |
51 | return | |
52 | } | |
53 | ||
54 | for { | |
55 | if processEntry.th32ProcessID == uint32(pid) { | |
56 | pe = &processEntry | |
57 | return | |
58 | } | |
59 | ||
60 | ok, _, e1 = Process32Next.Call(snapshot, uintptr(unsafe.Pointer(&processEntry))) | |
61 | if ok == 0 { | |
62 | err = fmt.Errorf("Process32Next: %v", e1) | |
63 | return | |
64 | } | |
65 | } | |
66 | } | |
67 | ||
68 | func getppid() (pid int, err error) { | |
69 | pe, err := getProcessEntry(os.Getpid()) | |
70 | if err != nil { | |
71 | return | |
72 | } | |
73 | ||
74 | pid = int(pe.th32ParentProcessID) | |
75 | return | |
76 | } | |
77 | ||
78 | // StartedByExplorer returns true if the program was invoked by the user double-clicking | |
79 | // on the executable from explorer.exe | |
80 | // | |
81 | // It is conservative and returns false if any of the internal calls fail. | |
82 | // It does not guarantee that the program was run from a terminal. It only can tell you | |
83 | // whether it was launched from explorer.exe | |
84 | func StartedByExplorer() bool { | |
85 | ppid, err := getppid() | |
86 | if err != nil { | |
87 | return false | |
88 | } | |
89 | ||
90 | pe, err := getProcessEntry(ppid) | |
91 | if err != nil { | |
92 | return false | |
93 | } | |
94 | ||
95 | name := syscall.UTF16ToString(pe.szExeFile[:]) | |
96 | return name == "explorer.exe" | |
97 | } |
0 | // +build windows | |
1 | // +build go1.4 | |
2 | ||
3 | package mousetrap | |
4 | ||
5 | import ( | |
6 | "os" | |
7 | "syscall" | |
8 | "unsafe" | |
9 | ) | |
10 | ||
11 | func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) { | |
12 | snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) | |
13 | if err != nil { | |
14 | return nil, err | |
15 | } | |
16 | defer syscall.CloseHandle(snapshot) | |
17 | var procEntry syscall.ProcessEntry32 | |
18 | procEntry.Size = uint32(unsafe.Sizeof(procEntry)) | |
19 | if err = syscall.Process32First(snapshot, &procEntry); err != nil { | |
20 | return nil, err | |
21 | } | |
22 | for { | |
23 | if procEntry.ProcessID == uint32(pid) { | |
24 | return &procEntry, nil | |
25 | } | |
26 | err = syscall.Process32Next(snapshot, &procEntry) | |
27 | if err != nil { | |
28 | return nil, err | |
29 | } | |
30 | } | |
31 | } | |
32 | ||
33 | // StartedByExplorer returns true if the program was invoked by the user double-clicking | |
34 | // on the executable from explorer.exe | |
35 | // | |
36 | // It is conservative and returns false if any of the internal calls fail. | |
37 | // It does not guarantee that the program was run from a terminal. It only can tell you | |
38 | // whether it was launched from explorer.exe | |
39 | func StartedByExplorer() bool { | |
40 | pe, err := getProcessEntry(os.Getppid()) | |
41 | if err != nil { | |
42 | return false | |
43 | } | |
44 | return "explorer.exe" == syscall.UTF16ToString(pe.ExeFile[:]) | |
45 | } |