├── .gitignore ├── .gitmodules ├── LICENSE.md ├── Makefile ├── README.md ├── backend.go ├── ci ├── build-artifacts └── build-artifacts.yml ├── cmd └── houdini │ └── main.go ├── container.go ├── container_linux.go ├── container_stub.go ├── container_test.go ├── go.mod ├── go.sum ├── houdini_suite_test.go ├── process ├── fanin_writer.go ├── fanout_writer.go ├── process.go ├── process_tracker.go ├── spawn.go └── spawn_windows.go ├── ptyutil ├── raw.go ├── winsize.go └── winsize_solaris.go └── win32 ├── win32_windows.go ├── win32_windows_386.go └── win32_windows_amd64.go /.gitignore: -------------------------------------------------------------------------------- 1 | .goempty/ 2 | dist/ 3 | deps/pkg/ 4 | deps/bin/ 5 | *.tar.gz 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vito/houdini/2a6a4c6dbbde1dc26ba101d199b8d027727ba936/.gitmodules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps clean 2 | 3 | darwin.tar.gz: dist/houdini dist/skeleton 4 | tar vczf darwin.tar.gz -C dist houdini skeleton 5 | 6 | dist/houdini: cmd/houdini/**/* 7 | go build -o dist/houdini ./cmd/houdini 8 | 9 | dist/skeleton/bin: 10 | mkdir -p dist/skeleton/bin 11 | 12 | dist/skeleton/workdir: 13 | mkdir -p dist/skeleton/workdir 14 | 15 | clean: 16 | rm -rf dist 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # houdini: the world's worst containerizer 2 | 3 | Houdini is effectively a no-op Garden backend portable to esoteric platforms 4 | like Darwin and Windows. 5 | 6 | For a Linux Garden backend, please use [Garden 7 | Linux](https://github.com/cloudfoundry-incubator/garden-linux) instead. 8 | 9 | Houdini makes no attempt to isolate containers from each other. It puts them 10 | in a working directory, and hopes for the best. 11 | 12 | On Windows, it will at least use [Job 13 | Objects](https://msdn.microsoft.com/en-us/library/windows/desktop/ms684161%28v=vs.85%29.aspx) 14 | to ensure processes are fully cleaned up. On OS X, there are basically no 15 | good ways to do this, so it doesn't bother. 16 | -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package houdini 2 | 3 | import ( 4 | "strconv" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "code.cloudfoundry.org/garden" 10 | "github.com/charlievieth/fs" 11 | ) 12 | 13 | type Backend struct { 14 | containersDir string 15 | 16 | containers map[string]*container 17 | containersL sync.RWMutex 18 | 19 | containerNum uint32 20 | } 21 | 22 | func NewBackend(containersDir string) *Backend { 23 | return &Backend{ 24 | containersDir: containersDir, 25 | 26 | containers: make(map[string]*container), 27 | 28 | containerNum: uint32(time.Now().UnixNano()), 29 | } 30 | } 31 | 32 | func (backend *Backend) Start() error { 33 | return fs.MkdirAll(backend.containersDir, 0755) 34 | } 35 | 36 | func (backend *Backend) Stop() error { 37 | containers, err := backend.Containers(nil) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | for _, container := range containers { 43 | err = backend.Destroy(container.Handle()) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (backend *Backend) GraceTime(c garden.Container) time.Duration { 53 | return c.(*container).currentGraceTime() 54 | } 55 | 56 | func (backend *Backend) Ping() error { 57 | return nil 58 | } 59 | 60 | func (backend *Backend) Capacity() (garden.Capacity, error) { 61 | println("NOT IMPLEMENTED: Capacity") 62 | return garden.Capacity{}, nil 63 | } 64 | 65 | func (backend *Backend) Create(spec garden.ContainerSpec) (garden.Container, error) { 66 | id := backend.generateContainerID() 67 | 68 | if spec.Handle == "" { 69 | spec.Handle = id 70 | } 71 | 72 | container, err := backend.newContainer(spec, id) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | err = container.setup() 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | backend.containersL.Lock() 83 | backend.containers[spec.Handle] = container 84 | backend.containersL.Unlock() 85 | 86 | return container, nil 87 | } 88 | 89 | func (backend *Backend) Destroy(handle string) error { 90 | backend.containersL.RLock() 91 | container, found := backend.containers[handle] 92 | backend.containersL.RUnlock() 93 | 94 | if !found { 95 | return garden.ContainerNotFoundError{Handle: handle} 96 | } 97 | 98 | err := container.Stop(false) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = container.cleanup() 104 | if err != nil { 105 | return err 106 | } 107 | 108 | backend.containersL.Lock() 109 | delete(backend.containers, handle) 110 | backend.containersL.Unlock() 111 | 112 | return nil 113 | } 114 | 115 | func (backend *Backend) Containers(filter garden.Properties) ([]garden.Container, error) { 116 | matchingContainers := []garden.Container{} 117 | 118 | backend.containersL.RLock() 119 | 120 | for _, container := range backend.containers { 121 | if containerHasProperties(container, filter) { 122 | matchingContainers = append(matchingContainers, container) 123 | } 124 | } 125 | 126 | backend.containersL.RUnlock() 127 | 128 | return matchingContainers, nil 129 | } 130 | 131 | func (backend *Backend) BulkInfo(handles []string) (map[string]garden.ContainerInfoEntry, error) { 132 | return map[string]garden.ContainerInfoEntry{}, nil 133 | } 134 | 135 | func (backend *Backend) BulkMetrics(handles []string) (map[string]garden.ContainerMetricsEntry, error) { 136 | return map[string]garden.ContainerMetricsEntry{}, nil 137 | } 138 | 139 | func (backend *Backend) Lookup(handle string) (garden.Container, error) { 140 | backend.containersL.RLock() 141 | container, found := backend.containers[handle] 142 | backend.containersL.RUnlock() 143 | 144 | if !found { 145 | return nil, garden.ContainerNotFoundError{Handle: handle} 146 | } 147 | 148 | return container, nil 149 | } 150 | 151 | func (backend *Backend) generateContainerID() string { 152 | containerNum := atomic.AddUint32(&backend.containerNum, 1) 153 | 154 | containerID := []byte{} 155 | 156 | var i uint64 157 | for i = 0; i < 11; i++ { 158 | containerID = strconv.AppendUint( 159 | containerID, 160 | (uint64(containerNum)>>(55-(i+1)*5))&31, 161 | 32, 162 | ) 163 | } 164 | 165 | return string(containerID) 166 | } 167 | 168 | func containerHasProperties(container *container, properties garden.Properties) bool { 169 | containerProps := container.currentProperties() 170 | 171 | for key, val := range properties { 172 | cval, ok := containerProps[key] 173 | if !ok { 174 | return false 175 | } 176 | 177 | if cval != val { 178 | return false 179 | } 180 | } 181 | 182 | return true 183 | } 184 | -------------------------------------------------------------------------------- /ci/build-artifacts: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e -x 4 | 5 | export GOPATH=$PWD/gopath 6 | export PATH=$PWD/bin:$PATH 7 | 8 | export DISTDIR=$PWD/release 9 | 10 | date +"%Y-%m-%d" > $DISTDIR/release-name 11 | 12 | cd $GOPATH/src/github.com/vito/houdini 13 | 14 | mkdir -p $DISTDIR/artifacts 15 | 16 | export GOPATH=$PWD/deps:$GOPATH 17 | 18 | GOOS=darwin GOARCH=amd64 \ 19 | go build -o $DISTDIR/artifacts/houdini_darwin_amd64 ./cmd/houdini 20 | 21 | GOOS=windows GOARCH=amd64 \ 22 | go build -o $DISTDIR/artifacts/houdini_windows_amd64.exe ./cmd/houdini 23 | 24 | GOOS=windows GOARCH=386 \ 25 | go build -o $DISTDIR/artifacts/houdini_windows_386.exe ./cmd/houdini 26 | 27 | GOOS=linux GOARCH=amd64 \ 28 | go build -o $DISTDIR/artifacts/houdini_linux_amd64 ./cmd/houdini 29 | 30 | GOOS=linux GOARCH=386 \ 31 | go build -o $DISTDIR/artifacts/houdini_linux_386 ./cmd/houdini 32 | -------------------------------------------------------------------------------- /ci/build-artifacts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | 4 | image_resource: 5 | type: docker-image 6 | source: {repository: golang} 7 | 8 | inputs: 9 | - name: houdini 10 | path: gopath/src/github.com/vito/houdini 11 | 12 | outputs: 13 | - name: release 14 | 15 | run: 16 | path: gopath/src/github.com/vito/houdini/ci/build-artifacts 17 | -------------------------------------------------------------------------------- /cmd/houdini/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | _ "net/http/pprof" 8 | "os" 9 | "os/signal" 10 | "path/filepath" 11 | "syscall" 12 | "time" 13 | 14 | "code.cloudfoundry.org/garden/server" 15 | "code.cloudfoundry.org/lager" 16 | "github.com/vito/houdini" 17 | ) 18 | 19 | var listenNetwork = flag.String( 20 | "listenNetwork", 21 | "tcp", 22 | "how to listen on the address (unix, tcp, etc.)", 23 | ) 24 | 25 | var listenAddr = flag.String( 26 | "listenAddr", 27 | "0.0.0.0:7777", 28 | "address to listen on", 29 | ) 30 | 31 | var debugListenAddress = flag.String( 32 | "debugListenAddress", 33 | "127.0.0.1", 34 | "address for the pprof debugger listen on", 35 | ) 36 | 37 | var debugListenPort = flag.Int( 38 | "debugListenPort", 39 | 7776, 40 | "port for the pprof debugger to listen on", 41 | ) 42 | 43 | var containerGraceTime = flag.Duration( 44 | "containerGraceTime", 45 | 5*time.Minute, 46 | "time after which to destroy idle containers", 47 | ) 48 | 49 | var containersDir = flag.String( 50 | "depot", 51 | "./containers", 52 | "directory in which to store containers", 53 | ) 54 | 55 | func main() { 56 | flag.Parse() 57 | 58 | logger := lager.NewLogger("houdini") 59 | logger.RegisterSink(lager.NewWriterSink(os.Stdout, lager.DEBUG)) 60 | 61 | depot, err := filepath.Abs(*containersDir) 62 | if err != nil { 63 | logger.Fatal("failed-to-determine-depot-dir", err) 64 | } 65 | 66 | backend := houdini.NewBackend(depot) 67 | 68 | gardenServer := server.New(*listenNetwork, *listenAddr, *containerGraceTime, backend, logger) 69 | 70 | err = gardenServer.Start() 71 | if err != nil { 72 | logger.Fatal("failed-to-start-server", err) 73 | } 74 | 75 | logger.Info("started", lager.Data{ 76 | "network": *listenNetwork, 77 | "addr": *listenAddr, 78 | }) 79 | 80 | signals := make(chan os.Signal, 1) 81 | 82 | go func() { 83 | <-signals 84 | gardenServer.Stop() 85 | os.Exit(0) 86 | }() 87 | 88 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 89 | 90 | debugListenAddr := fmt.Sprintf("%s:%d", *debugListenAddress, *debugListenPort) 91 | 92 | err = http.ListenAndServe(debugListenAddr, nil) 93 | if err != nil { 94 | logger.Fatal("failed-to-start-debug-server", err) 95 | } 96 | 97 | select {} 98 | } 99 | -------------------------------------------------------------------------------- /container.go: -------------------------------------------------------------------------------- 1 | package houdini 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "code.cloudfoundry.org/garden" 14 | "github.com/charlievieth/fs" 15 | "github.com/concourse/go-archive/tarfs" 16 | "github.com/vito/houdini/process" 17 | ) 18 | 19 | type UndefinedPropertyError struct { 20 | Key string 21 | } 22 | 23 | func (err UndefinedPropertyError) Error() string { 24 | return fmt.Sprintf("property does not exist: %s", err.Key) 25 | } 26 | 27 | type container struct { 28 | spec garden.ContainerSpec 29 | 30 | handle string 31 | 32 | workDir string 33 | hasRootfs bool 34 | 35 | properties garden.Properties 36 | propertiesL sync.RWMutex 37 | 38 | env []string 39 | 40 | processTracker process.ProcessTracker 41 | 42 | graceTime time.Duration 43 | graceTimeL sync.RWMutex 44 | } 45 | 46 | func (backend *Backend) newContainer(spec garden.ContainerSpec, id string) (*container, error) { 47 | var workDir string 48 | var hasRootfs bool 49 | if spec.RootFSPath != "" { 50 | rootfsURI, err := url.Parse(spec.RootFSPath) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | switch rootfsURI.Scheme { 56 | case "raw": 57 | workDir = rootfsURI.Path 58 | hasRootfs = true 59 | default: 60 | return nil, fmt.Errorf("unsupported rootfs uri (must be raw://): %s", spec.RootFSPath) 61 | } 62 | } else { 63 | workDir = filepath.Join(backend.containersDir, id) 64 | 65 | err := fs.MkdirAll(workDir, 0755) 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | 71 | properties := spec.Properties 72 | if properties == nil { 73 | properties = garden.Properties{} 74 | } 75 | 76 | return &container{ 77 | spec: spec, 78 | 79 | handle: spec.Handle, 80 | 81 | workDir: workDir, 82 | hasRootfs: hasRootfs, 83 | 84 | properties: properties, 85 | 86 | env: spec.Env, 87 | 88 | processTracker: process.NewTracker(), 89 | }, nil 90 | } 91 | 92 | func (container *container) cleanup() error { 93 | if !container.hasRootfs { 94 | return fs.RemoveAll(container.workDir) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (container *container) Handle() string { 101 | return container.handle 102 | } 103 | 104 | func (container *container) Stop(kill bool) error { 105 | return container.processTracker.Stop(kill) 106 | } 107 | 108 | func (container *container) Info() (garden.ContainerInfo, error) { return garden.ContainerInfo{}, nil } 109 | 110 | func (container *container) StreamIn(spec garden.StreamInSpec) error { 111 | finalDestination := filepath.Join(container.workDir, filepath.FromSlash(spec.Path)) 112 | 113 | err := fs.MkdirAll(finalDestination, 0755) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | err = tarfs.Extract(spec.TarStream, finalDestination) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (container *container) StreamOut(spec garden.StreamOutSpec) (io.ReadCloser, error) { 127 | if strings.HasSuffix(spec.Path, "/") { 128 | spec.Path += "." 129 | } 130 | 131 | absoluteSource := container.workDir + string(os.PathSeparator) + filepath.FromSlash(spec.Path) 132 | 133 | r, w := io.Pipe() 134 | 135 | errs := make(chan error, 1) 136 | go func() { 137 | errs <- tarfs.Compress(w, filepath.Dir(absoluteSource), filepath.Base(absoluteSource)) 138 | _ = w.Close() 139 | }() 140 | 141 | return waitCloser{ 142 | ReadCloser: r, 143 | wait: errs, 144 | }, nil 145 | } 146 | 147 | type waitCloser struct { 148 | io.ReadCloser 149 | wait <-chan error 150 | } 151 | 152 | func (c waitCloser) Close() error { 153 | err := c.ReadCloser.Close() 154 | if err != nil { 155 | return err 156 | } 157 | 158 | return <-c.wait 159 | } 160 | 161 | func (container *container) LimitBandwidth(limits garden.BandwidthLimits) error { return nil } 162 | 163 | func (container *container) CurrentBandwidthLimits() (garden.BandwidthLimits, error) { 164 | return garden.BandwidthLimits{}, nil 165 | } 166 | 167 | func (container *container) LimitCPU(limits garden.CPULimits) error { return nil } 168 | 169 | func (container *container) CurrentCPULimits() (garden.CPULimits, error) { 170 | return garden.CPULimits{}, nil 171 | } 172 | 173 | func (container *container) LimitDisk(limits garden.DiskLimits) error { return nil } 174 | 175 | func (container *container) CurrentDiskLimits() (garden.DiskLimits, error) { 176 | return garden.DiskLimits{}, nil 177 | } 178 | 179 | func (container *container) LimitMemory(limits garden.MemoryLimits) error { return nil } 180 | 181 | func (container *container) CurrentMemoryLimits() (garden.MemoryLimits, error) { 182 | return garden.MemoryLimits{}, nil 183 | } 184 | 185 | func (container *container) NetIn(hostPort, containerPort uint32) (uint32, uint32, error) { 186 | return 0, 0, nil 187 | } 188 | 189 | func (container *container) NetOut(garden.NetOutRule) error { return nil } 190 | 191 | func (container *container) BulkNetOut([]garden.NetOutRule) error { return nil } 192 | 193 | func (container *container) Run(spec garden.ProcessSpec, processIO garden.ProcessIO) (garden.Process, error) { 194 | cmd, err := container.cmd(spec) 195 | if err != nil { 196 | return nil, err 197 | } 198 | 199 | return container.processTracker.Run( 200 | spec.ID, 201 | cmd, 202 | processIO, 203 | spec.TTY, 204 | ) 205 | } 206 | 207 | func (container *container) Attach(processID string, processIO garden.ProcessIO) (garden.Process, error) { 208 | return container.processTracker.Attach(processID, processIO) 209 | } 210 | 211 | func (container *container) Property(name string) (string, error) { 212 | container.propertiesL.RLock() 213 | property, found := container.properties[name] 214 | container.propertiesL.RUnlock() 215 | 216 | if !found { 217 | return "", UndefinedPropertyError{name} 218 | } 219 | 220 | return property, nil 221 | } 222 | 223 | func (container *container) SetProperty(name string, value string) error { 224 | container.propertiesL.Lock() 225 | container.properties[name] = value 226 | container.propertiesL.Unlock() 227 | 228 | return nil 229 | } 230 | 231 | func (container *container) RemoveProperty(name string) error { 232 | container.propertiesL.Lock() 233 | defer container.propertiesL.Unlock() 234 | 235 | _, found := container.properties[name] 236 | if !found { 237 | return UndefinedPropertyError{name} 238 | } 239 | 240 | delete(container.properties, name) 241 | 242 | return nil 243 | } 244 | 245 | func (container *container) Properties() (garden.Properties, error) { 246 | return container.currentProperties(), nil 247 | } 248 | 249 | func (container *container) Metrics() (garden.Metrics, error) { 250 | return garden.Metrics{}, nil 251 | } 252 | 253 | func (container *container) SetGraceTime(t time.Duration) error { 254 | container.graceTimeL.Lock() 255 | container.graceTime = t 256 | container.graceTimeL.Unlock() 257 | return nil 258 | } 259 | 260 | func (container *container) currentProperties() garden.Properties { 261 | properties := garden.Properties{} 262 | 263 | container.propertiesL.RLock() 264 | 265 | for k, v := range container.properties { 266 | properties[k] = v 267 | } 268 | 269 | container.propertiesL.RUnlock() 270 | 271 | return properties 272 | } 273 | 274 | func (container *container) currentGraceTime() time.Duration { 275 | container.graceTimeL.RLock() 276 | defer container.graceTimeL.RUnlock() 277 | return container.graceTime 278 | } 279 | -------------------------------------------------------------------------------- /container_linux.go: -------------------------------------------------------------------------------- 1 | package houdini 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | "syscall" 10 | 11 | "code.cloudfoundry.org/garden" 12 | ) 13 | 14 | func (container *container) setup() error { 15 | if container.hasRootfs { 16 | for _, dir := range []string{"/proc", "/dev", "/sys"} { 17 | dest := filepath.Join(container.workDir, dir) 18 | 19 | err := os.MkdirAll(dest, 0755) 20 | if err != nil { 21 | return fmt.Errorf("failed to create target for bind mount: %s", err) 22 | } 23 | 24 | err = syscall.Mount(dir, dest, "none", syscall.MS_BIND|syscall.MS_RDONLY, "") 25 | if err != nil { 26 | return err 27 | } 28 | } 29 | 30 | for _, file := range []string{"/etc/resolv.conf", "/etc/hosts"} { 31 | dest := filepath.Join(container.workDir, file) 32 | 33 | f, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0644) 34 | if err != nil { 35 | return fmt.Errorf("failed to create target for bind mount: %s", err) 36 | } 37 | 38 | err = f.Close() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = syscall.Mount(file, dest, "none", syscall.MS_BIND|syscall.MS_RDONLY, "") 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | } 49 | 50 | for _, bm := range container.spec.BindMounts { 51 | dest := filepath.Join(container.workDir, bm.DstPath) 52 | 53 | err := os.MkdirAll(dest, 0755) 54 | if err != nil { 55 | return fmt.Errorf("failed to create target for bind mount: %s", err) 56 | } 57 | 58 | flags := uintptr(syscall.MS_BIND) 59 | if bm.Mode == garden.BindMountModeRO { 60 | flags |= syscall.MS_RDONLY 61 | } 62 | 63 | err = syscall.Mount(bm.SrcPath, dest, "none", flags, "") 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | const defaultRootPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 73 | // const defaultPath = "/usr/local/bin:/usr/bin:/bin" 74 | 75 | func (container *container) path() string { 76 | var path string 77 | for _, env := range container.env { 78 | segs := strings.SplitN(env, "=", 2) 79 | if len(segs) < 2 { 80 | continue 81 | } 82 | 83 | if segs[0] == "PATH" { 84 | path = segs[1] 85 | } 86 | } 87 | 88 | if !container.hasRootfs { 89 | if path == "" { 90 | path = os.Getenv("PATH") 91 | } 92 | 93 | return path 94 | } 95 | 96 | if path == "" { 97 | // assume running as root for now, since Houdini doesn't currently support 98 | // running as a user 99 | path = defaultRootPath 100 | } 101 | 102 | var scopedPath string 103 | for _, dir := range filepath.SplitList(path) { 104 | if scopedPath != "" { 105 | scopedPath += string(filepath.ListSeparator) 106 | } 107 | 108 | scopedPath += container.workDir + dir 109 | } 110 | 111 | return scopedPath 112 | } 113 | 114 | func (container *container) cmd(spec garden.ProcessSpec) (*exec.Cmd, error) { 115 | var cmd *exec.Cmd 116 | 117 | if container.hasRootfs { 118 | path := spec.Path 119 | 120 | if !strings.Contains(path, "/") { 121 | // find executable within container's $PATH 122 | 123 | absPath, err := lookPath(path, container.path()) 124 | if err != nil { 125 | return nil, garden.ExecutableNotFoundError{ 126 | Message: err.Error(), 127 | } 128 | } 129 | 130 | // correct path so that it's absolute from the rootfs 131 | path = strings.TrimPrefix(absPath, container.workDir) 132 | } 133 | 134 | cmd = exec.Command(path, spec.Args...) 135 | 136 | if spec.Dir != "" { 137 | cmd.Dir = spec.Dir 138 | } else { 139 | cmd.Dir = "/" 140 | } 141 | 142 | cmd.SysProcAttr = &syscall.SysProcAttr{ 143 | Chroot: container.workDir, 144 | } 145 | } else { 146 | cmd = exec.Command(spec.Path, spec.Args...) 147 | cmd.Dir = filepath.Join(container.workDir, spec.Dir) 148 | } 149 | 150 | cmd.Env = append(os.Environ(), append(container.env, spec.Env...)...) 151 | 152 | return cmd, nil 153 | } 154 | 155 | func findExecutable(file string) error { 156 | d, err := os.Stat(file) 157 | if err != nil { 158 | return err 159 | } 160 | if m := d.Mode(); !m.IsDir() && m&0111 != 0 { 161 | return nil 162 | } 163 | return os.ErrPermission 164 | } 165 | 166 | // based on exec.LookPath from stdlib 167 | func lookPath(file string, path string) (string, error) { 168 | if strings.Contains(file, "/") { 169 | err := findExecutable(file) 170 | if err == nil { 171 | return file, nil 172 | } 173 | return "", &exec.Error{Name: file, Err: err} 174 | } 175 | 176 | for _, dir := range filepath.SplitList(path) { 177 | if dir == "" { 178 | // Unix shell semantics: path element "" means "." 179 | dir = "." 180 | } 181 | path := filepath.Join(dir, file) 182 | if err := findExecutable(path); err == nil { 183 | return path, nil 184 | } 185 | } 186 | 187 | return "", &exec.Error{Name: file, Err: exec.ErrNotFound} 188 | } 189 | -------------------------------------------------------------------------------- /container_stub.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package houdini 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | 12 | "code.cloudfoundry.org/garden" 13 | ) 14 | 15 | func (container *container) setup() error { 16 | for _, bm := range container.spec.BindMounts { 17 | if bm.Mode == garden.BindMountModeRO { 18 | return errors.New("read-only bind mounts are unsupported") 19 | } 20 | 21 | dest := filepath.Join(container.workDir, bm.DstPath) 22 | _, err := os.Stat(dest) 23 | if err == nil { 24 | err = os.Remove(dest) 25 | if err != nil { 26 | return fmt.Errorf("failed to remove destination for bind mount: %s", err) 27 | } 28 | } 29 | 30 | err = os.MkdirAll(filepath.Dir(dest), 0755) 31 | if err != nil { 32 | return fmt.Errorf("failed to create parent dir for bind mount: %s", err) 33 | } 34 | 35 | absSrc, err := filepath.Abs(bm.SrcPath) 36 | if err != nil { 37 | return fmt.Errorf("failed to resolve source path: %s", err) 38 | } 39 | 40 | // windows symlinks ("junctions") support directories, but not hard-links 41 | // darwin hardlinks have strange restrictions 42 | // symlinks behave reasonably similar to bind mounts on OS X (unlike Linux) 43 | err = os.Symlink(absSrc, dest) 44 | if err != nil { 45 | return fmt.Errorf("failed to create hardlink for bind mount: %s", err) 46 | } 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (container *container) cmd(spec garden.ProcessSpec) (*exec.Cmd, error) { 53 | cmd := exec.Command(filepath.FromSlash(spec.Path), spec.Args...) 54 | cmd.Env = append(os.Environ(), append(container.env, spec.Env...)...) 55 | cmd.Dir = filepath.Join(container.workDir, filepath.FromSlash(spec.Dir)) 56 | 57 | return cmd, nil 58 | } 59 | -------------------------------------------------------------------------------- /container_test.go: -------------------------------------------------------------------------------- 1 | package houdini_test 2 | 3 | import ( 4 | "io" 5 | 6 | "code.cloudfoundry.org/garden" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Container", func() { 13 | var container garden.Container 14 | 15 | BeforeEach(func() { 16 | var err error 17 | container, err = backend.Create(garden.ContainerSpec{}) 18 | Expect(err).ToNot(HaveOccurred()) 19 | }) 20 | 21 | AfterEach(func() { 22 | err := backend.Destroy(container.Handle()) 23 | Expect(err).ToNot(HaveOccurred()) 24 | }) 25 | 26 | Describe("Streaming", func() { 27 | Context("between containers", func() { 28 | var destinationContainer garden.Container 29 | 30 | BeforeEach(func() { 31 | var err error 32 | destinationContainer, err = backend.Create(garden.ContainerSpec{}) 33 | Expect(err).ToNot(HaveOccurred()) 34 | }) 35 | 36 | AfterEach(func() { 37 | err := backend.Destroy(destinationContainer.Handle()) 38 | Expect(err).ToNot(HaveOccurred()) 39 | }) 40 | 41 | It("can transfer between containers", func() { 42 | process, err := container.Run(garden.ProcessSpec{ 43 | Path: "sh", 44 | Args: []string{ 45 | "-exc", 46 | ` 47 | touch a 48 | touch b 49 | mkdir foo/ 50 | touch foo/in-foo-a 51 | touch foo/in-foo-b 52 | `, 53 | }, 54 | }, garden.ProcessIO{ 55 | Stdout: GinkgoWriter, 56 | Stderr: GinkgoWriter, 57 | }) 58 | Expect(err).ToNot(HaveOccurred()) 59 | Expect(process.Wait()).To(Equal(0)) 60 | 61 | out, err := container.StreamOut(garden.StreamOutSpec{ 62 | Path: ".", 63 | }) 64 | Expect(err).ToNot(HaveOccurred()) 65 | 66 | err = destinationContainer.StreamIn(garden.StreamInSpec{ 67 | Path: ".", 68 | TarStream: out, 69 | }) 70 | Expect(err).ToNot(HaveOccurred()) 71 | 72 | nothing := make([]byte, 1) 73 | n, err := out.Read(nothing) 74 | Expect(n).To(Equal(0)) 75 | Expect(err).To(Equal(io.EOF)) 76 | 77 | checkTree, err := destinationContainer.Run(garden.ProcessSpec{ 78 | Path: "sh", 79 | Args: []string{ 80 | "-exc", 81 | ` 82 | find . 83 | test -e a 84 | test -e b 85 | test -e foo/in-foo-a 86 | test -e foo/in-foo-b 87 | `, 88 | }, 89 | }, garden.ProcessIO{ 90 | Stdout: GinkgoWriter, 91 | Stderr: GinkgoWriter, 92 | }) 93 | Expect(err).ToNot(HaveOccurred()) 94 | Expect(checkTree.Wait()).To(Equal(0)) 95 | }) 96 | 97 | It("treates a trailing slash as /.", func() { 98 | process, err := container.Run(garden.ProcessSpec{ 99 | Path: "sh", 100 | Args: []string{ 101 | "-exc", 102 | ` 103 | touch a 104 | touch b 105 | mkdir foo/ 106 | touch foo/in-foo-a 107 | touch foo/in-foo-b 108 | `, 109 | }, 110 | }, garden.ProcessIO{ 111 | Stdout: GinkgoWriter, 112 | Stderr: GinkgoWriter, 113 | }) 114 | Expect(err).ToNot(HaveOccurred()) 115 | Expect(process.Wait()).To(Equal(0)) 116 | 117 | out, err := container.StreamOut(garden.StreamOutSpec{ 118 | Path: "foo/", 119 | }) 120 | Expect(err).ToNot(HaveOccurred()) 121 | 122 | err = destinationContainer.StreamIn(garden.StreamInSpec{ 123 | Path: ".", 124 | TarStream: out, 125 | }) 126 | Expect(err).ToNot(HaveOccurred()) 127 | 128 | nothing := make([]byte, 1) 129 | n, err := out.Read(nothing) 130 | Expect(n).To(Equal(0)) 131 | Expect(err).To(Equal(io.EOF)) 132 | 133 | checkTree, err := destinationContainer.Run(garden.ProcessSpec{ 134 | Path: "sh", 135 | Args: []string{ 136 | "-exc", 137 | ` 138 | find . 139 | test -e in-foo-a 140 | test -e in-foo-b 141 | `, 142 | }, 143 | }, garden.ProcessIO{ 144 | Stdout: GinkgoWriter, 145 | Stderr: GinkgoWriter, 146 | }) 147 | Expect(err).ToNot(HaveOccurred()) 148 | Expect(checkTree.Wait()).To(Equal(0)) 149 | }) 150 | }) 151 | }) 152 | }) 153 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vito/houdini 2 | 3 | go 1.21 4 | 5 | require ( 6 | code.cloudfoundry.org/garden v0.0.0-20230322140108-76fb7bb00c07 7 | code.cloudfoundry.org/lager v1.1.1-0.20230321195817-3d52f427a2d2 8 | github.com/charlievieth/fs v0.0.0-20170613215519-7dc373669fa1 9 | github.com/concourse/go-archive v1.0.0 10 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d 11 | github.com/onsi/ginkgo/v2 v2.12.1 12 | github.com/onsi/gomega v1.27.10 13 | github.com/pkg/term v1.2.0-beta.2 14 | golang.org/x/sys v0.12.0 15 | ) 16 | 17 | require ( 18 | github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f // indirect 19 | github.com/go-logr/logr v1.2.4 // indirect 20 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 21 | github.com/google/go-cmp v0.5.9 // indirect 22 | github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect 23 | github.com/openzipkin/zipkin-go v0.4.1 // indirect 24 | github.com/tedsuo/rata v1.0.1-0.20170830210128-07d200713958 // indirect 25 | golang.org/x/net v0.14.0 // indirect 26 | golang.org/x/text v0.12.0 // indirect 27 | golang.org/x/tools v0.12.0 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | code.cloudfoundry.org/garden v0.0.0-20230322140108-76fb7bb00c07 h1:2dI6j3X30nChIzQhVYtR9k+6Gy5TNvS+pFeG9RTyq4U= 2 | code.cloudfoundry.org/garden v0.0.0-20230322140108-76fb7bb00c07/go.mod h1:Wryl/I6XaIc53BBm3YJ+Qxa4DQIPR6xbf9wh0prbMHM= 3 | code.cloudfoundry.org/lager v1.1.1-0.20230321195817-3d52f427a2d2 h1:/XzZDmyGGMbVARg2I8/gooQHUZrnbFjLN6blLWxdcXU= 4 | code.cloudfoundry.org/lager v1.1.1-0.20230321195817-3d52f427a2d2/go.mod h1:4//yL08XH9xro2/KMV6cQ0INl8E+5+XFIVcXJrwrGao= 5 | github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f h1:gOO/tNZMjjvTKZWpY7YnXC72ULNLErRtp94LountVE8= 6 | github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= 7 | github.com/charlievieth/fs v0.0.0-20170613215519-7dc373669fa1 h1:vTlpHKxJqykyKdW9bkrDJNWeKNuSIAJ0TP/K4lRsz/Q= 8 | github.com/charlievieth/fs v0.0.0-20170613215519-7dc373669fa1/go.mod h1:sAoA1zHCH4FJPE2gne5iBiiVG66U7Nyp6JqlOo+FEyg= 9 | github.com/concourse/go-archive v1.0.0 h1:nRejB54QZL8UwCKR5UlenYqwkEKN1A7khsarI5oRBkY= 10 | github.com/concourse/go-archive v1.0.0/go.mod h1:Xfo080IPQBmVz3I5ehjCddW3phA2mwv0NFwlpjf5CO8= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 15 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 16 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 17 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 18 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 19 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 20 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 21 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 22 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 23 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 24 | github.com/google/pprof v0.0.0-20230309165930-d61513b1440d h1:um9/pc7tKMINFfP1eE7Wv6PRGXlcCSJkVajF7KJw3uQ= 25 | github.com/google/pprof v0.0.0-20230309165930-d61513b1440d/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= 26 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 28 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 29 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 30 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 31 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 32 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 33 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 34 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 35 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 36 | github.com/onsi/ginkgo/v2 v2.12.1 h1:uHNEO1RP2SpuZApSkel9nEh1/Mu+hmQe7Q+Pepg5OYA= 37 | github.com/onsi/ginkgo/v2 v2.12.1/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= 38 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 39 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 40 | github.com/openzipkin/zipkin-go v0.4.1 h1:kNd/ST2yLLWhaWrkgchya40TJabe8Hioj9udfPcEO5A= 41 | github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM= 42 | github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= 43 | github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 48 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/tedsuo/rata v1.0.1-0.20170830210128-07d200713958 h1:mueRRuRjR35dEOkHdhpoRcruNgBz0ohG659HxxmcAwA= 50 | github.com/tedsuo/rata v1.0.1-0.20170830210128-07d200713958/go.mod h1:X47ELzhOoLbfFIY0Cql9P6yo3Cdwf2CMX3FVZxRzJPc= 51 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 52 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 53 | golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= 54 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 55 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 57 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= 59 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 60 | golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= 61 | golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= 62 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 63 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 66 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 67 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 68 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 69 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 71 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | -------------------------------------------------------------------------------- /houdini_suite_test.go: -------------------------------------------------------------------------------- 1 | package houdini_test 2 | 3 | import ( 4 | "os" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | "github.com/vito/houdini" 9 | 10 | "testing" 11 | ) 12 | 13 | var depotDir string 14 | var backend *houdini.Backend 15 | 16 | var _ = BeforeEach(func() { 17 | var err error 18 | depotDir, err = os.MkdirTemp("", "depot") 19 | Expect(err).ToNot(HaveOccurred()) 20 | 21 | backend = houdini.NewBackend(depotDir) 22 | 23 | err = backend.Start() 24 | Expect(err).ToNot(HaveOccurred()) 25 | }) 26 | 27 | var _ = AfterEach(func() { 28 | backend.Stop() 29 | 30 | err := os.RemoveAll(depotDir) 31 | Expect(err).ToNot(HaveOccurred()) 32 | }) 33 | 34 | func TestHoudini(t *testing.T) { 35 | RegisterFailHandler(Fail) 36 | RunSpecs(t, "Houdini Suite") 37 | } 38 | -------------------------------------------------------------------------------- /process/fanin_writer.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | type faninWriter struct { 10 | w io.WriteCloser 11 | closed bool 12 | writeL sync.Mutex 13 | 14 | hasSink chan struct{} 15 | } 16 | 17 | func (w *faninWriter) Write(data []byte) (int, error) { 18 | <-w.hasSink 19 | 20 | w.writeL.Lock() 21 | 22 | if w.closed { 23 | return 0, errors.New("write after close") 24 | } 25 | 26 | defer w.writeL.Unlock() 27 | 28 | return w.w.Write(data) 29 | } 30 | 31 | func (w *faninWriter) Close() error { 32 | <-w.hasSink 33 | 34 | w.writeL.Lock() 35 | 36 | if w.closed { 37 | return errors.New("closed twice") 38 | } 39 | 40 | w.closed = true 41 | 42 | defer w.writeL.Unlock() 43 | 44 | return w.w.Close() 45 | } 46 | 47 | func (w *faninWriter) AddSink(sink io.WriteCloser) { 48 | w.w = sink 49 | close(w.hasSink) 50 | } 51 | 52 | func (w *faninWriter) AddSource(source io.Reader) { 53 | go func() { 54 | _, err := io.Copy(w, source) 55 | if err == nil { 56 | w.Close() 57 | } 58 | }() 59 | } 60 | -------------------------------------------------------------------------------- /process/fanout_writer.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | type fanoutWriter struct { 10 | sinks []io.Writer 11 | closed bool 12 | sinksL sync.Mutex 13 | } 14 | 15 | func (w *fanoutWriter) Write(data []byte) (int, error) { 16 | w.sinksL.Lock() 17 | 18 | if w.closed { 19 | return 0, errors.New("write after close") 20 | } 21 | 22 | // the sinks should be nonblocking and never actually error; 23 | // we can assume lossiness here, and do this all within the lock 24 | for _, s := range w.sinks { 25 | s.Write(data) 26 | } 27 | 28 | w.sinksL.Unlock() 29 | 30 | return len(data), nil 31 | } 32 | 33 | func (w *fanoutWriter) AddSink(sink io.Writer) { 34 | w.sinksL.Lock() 35 | 36 | if !w.closed { 37 | w.sinks = append(w.sinks, sink) 38 | } 39 | 40 | w.sinksL.Unlock() 41 | } 42 | -------------------------------------------------------------------------------- /process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "os/exec" 5 | "sync" 6 | 7 | "code.cloudfoundry.org/garden" 8 | ) 9 | 10 | type process interface { 11 | Signal(garden.Signal) error 12 | Wait() (int, error) 13 | SetWindowSize(garden.WindowSize) error 14 | } 15 | 16 | type Process struct { 17 | id string 18 | 19 | process process 20 | 21 | waiting *sync.Once 22 | exitStatus int 23 | exitErr error 24 | 25 | stdin *faninWriter 26 | stdout *fanoutWriter 27 | stderr *fanoutWriter 28 | } 29 | 30 | func NewProcess(id string) *Process { 31 | return &Process{ 32 | id: id, 33 | 34 | waiting: &sync.Once{}, 35 | 36 | stdin: &faninWriter{hasSink: make(chan struct{})}, 37 | stdout: &fanoutWriter{}, 38 | stderr: &fanoutWriter{}, 39 | } 40 | } 41 | 42 | func (p *Process) ID() string { 43 | return p.id 44 | } 45 | 46 | func (p *Process) Wait() (int, error) { 47 | p.waiting.Do(func() { 48 | p.exitStatus, p.exitErr = p.process.Wait() 49 | 50 | // don't leak stdin pipe 51 | p.stdin.Close() 52 | }) 53 | 54 | return p.exitStatus, p.exitErr 55 | } 56 | 57 | func (p *Process) SetTTY(tty garden.TTYSpec) error { 58 | if tty.WindowSize != nil { 59 | return p.process.SetWindowSize(*tty.WindowSize) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (p *Process) Start(cmd *exec.Cmd, tty *garden.TTYSpec) error { 66 | process, stdin, err := spawn(cmd, tty, p.stdout, p.stderr) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | p.stdin.AddSink(stdin) 72 | 73 | p.process = process 74 | 75 | return nil 76 | } 77 | 78 | func (p *Process) Attach(processIO garden.ProcessIO) { 79 | if processIO.Stdin != nil { 80 | p.stdin.AddSource(processIO.Stdin) 81 | } 82 | 83 | if processIO.Stdout != nil { 84 | p.stdout.AddSink(processIO.Stdout) 85 | } 86 | 87 | if processIO.Stderr != nil { 88 | p.stderr.AddSink(processIO.Stderr) 89 | } 90 | } 91 | 92 | func (p *Process) Signal(signal garden.Signal) error { 93 | return p.process.Signal(signal) 94 | } 95 | -------------------------------------------------------------------------------- /process/process_tracker.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "sync" 7 | "time" 8 | 9 | "code.cloudfoundry.org/garden" 10 | "github.com/nu7hatch/gouuid" 11 | ) 12 | 13 | type ProcessTracker interface { 14 | Run(string, *exec.Cmd, garden.ProcessIO, *garden.TTYSpec) (garden.Process, error) 15 | Attach(string, garden.ProcessIO) (garden.Process, error) 16 | Restore(processID string) 17 | ActiveProcesses() []garden.Process 18 | Stop(kill bool) error 19 | } 20 | 21 | type processTracker struct { 22 | processes map[string]*Process 23 | processesMutex *sync.RWMutex 24 | } 25 | 26 | type UnknownProcessError struct { 27 | ProcessID string 28 | } 29 | 30 | func (e UnknownProcessError) Error() string { 31 | return fmt.Sprintf("unknown process: %s", e.ProcessID) 32 | } 33 | 34 | func NewTracker() ProcessTracker { 35 | return &processTracker{ 36 | processes: make(map[string]*Process), 37 | processesMutex: new(sync.RWMutex), 38 | } 39 | } 40 | 41 | func (t *processTracker) Run(passedID string, cmd *exec.Cmd, processIO garden.ProcessIO, tty *garden.TTYSpec) (garden.Process, error) { 42 | t.processesMutex.Lock() 43 | defer t.processesMutex.Unlock() 44 | 45 | processID := passedID 46 | if processID == "" { 47 | uuid, err := uuid.NewV4() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | processID = uuid.String() 53 | } 54 | 55 | process := NewProcess(processID) 56 | 57 | process.Attach(processIO) 58 | 59 | err := process.Start(cmd, tty) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | t.processes[processID] = process 65 | 66 | return process, nil 67 | } 68 | 69 | func (t *processTracker) Attach(processID string, processIO garden.ProcessIO) (garden.Process, error) { 70 | t.processesMutex.RLock() 71 | process, ok := t.processes[processID] 72 | t.processesMutex.RUnlock() 73 | 74 | if !ok { 75 | return nil, UnknownProcessError{processID} 76 | } 77 | 78 | process.Attach(processIO) 79 | 80 | go t.waitAndReap(processID) 81 | 82 | return process, nil 83 | } 84 | 85 | func (t *processTracker) Restore(processID string) { 86 | t.processesMutex.Lock() 87 | 88 | process := NewProcess(processID) 89 | 90 | t.processes[processID] = process 91 | 92 | go t.waitAndReap(processID) 93 | 94 | t.processesMutex.Unlock() 95 | } 96 | 97 | func (t *processTracker) ActiveProcesses() []garden.Process { 98 | t.processesMutex.RLock() 99 | defer t.processesMutex.RUnlock() 100 | 101 | processes := make([]garden.Process, len(t.processes)) 102 | 103 | i := 0 104 | for _, process := range t.processes { 105 | processes[i] = process 106 | i++ 107 | } 108 | 109 | return processes 110 | } 111 | 112 | func (t *processTracker) Stop(kill bool) error { 113 | t.processesMutex.RLock() 114 | 115 | processes := make([]*Process, len(t.processes)) 116 | 117 | i := 0 118 | for _, process := range t.processes { 119 | processes[i] = process 120 | i++ 121 | } 122 | 123 | t.processesMutex.RUnlock() 124 | 125 | wait := new(sync.WaitGroup) 126 | wait.Add(len(processes)) 127 | 128 | for _, process := range processes { 129 | exited := make(chan struct{}) 130 | 131 | go func(process *Process) { 132 | process.Wait() 133 | close(exited) 134 | wait.Done() 135 | }(process) 136 | 137 | if kill { 138 | process.Signal(garden.SignalKill) 139 | } else { 140 | process.Signal(garden.SignalTerminate) 141 | 142 | go func(process *Process) { 143 | select { 144 | case <-exited: 145 | case <-time.After(10 * time.Second): 146 | process.Signal(garden.SignalKill) 147 | } 148 | }(process) 149 | } 150 | } 151 | 152 | wait.Wait() 153 | 154 | return nil 155 | } 156 | 157 | func (t *processTracker) waitAndReap(processID string) { 158 | t.processesMutex.RLock() 159 | process, ok := t.processes[processID] 160 | t.processesMutex.RUnlock() 161 | 162 | if !ok { 163 | return 164 | } 165 | 166 | process.Wait() 167 | 168 | t.unregister(processID) 169 | } 170 | 171 | func (t *processTracker) unregister(processID string) { 172 | t.processesMutex.Lock() 173 | defer t.processesMutex.Unlock() 174 | 175 | delete(t.processes, processID) 176 | } 177 | -------------------------------------------------------------------------------- /process/spawn.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package process 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | 11 | "code.cloudfoundry.org/garden" 12 | "github.com/vito/houdini/ptyutil" 13 | "github.com/pkg/term/termios" 14 | ) 15 | 16 | func spawn(cmd *exec.Cmd, ttySpec *garden.TTYSpec, stdout io.Writer, stderr io.Writer) (process, io.WriteCloser, error) { 17 | var stdin io.WriteCloser 18 | var err error 19 | 20 | var processPty *os.File 21 | 22 | if ttySpec != nil { 23 | pty, tty, err := termios.Pty() 24 | if err != nil { 25 | return nil, nil, err 26 | } 27 | 28 | // close our end of the tty after the process has spawned 29 | defer tty.Close() 30 | 31 | processPty = pty 32 | stdin = pty 33 | 34 | windowColumns := 80 35 | windowRows := 24 36 | if ttySpec.WindowSize != nil { 37 | windowColumns = ttySpec.WindowSize.Columns 38 | windowRows = ttySpec.WindowSize.Rows 39 | } 40 | 41 | ptyutil.SetWinSize(pty, windowColumns, windowRows) 42 | 43 | cmd.Stdin = tty 44 | cmd.Stdout = tty 45 | cmd.Stderr = tty 46 | 47 | go io.Copy(stdout, pty) 48 | } else { 49 | stdin, err = cmd.StdinPipe() 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | cmd.Stdout = stdout 55 | cmd.Stderr = stderr 56 | } 57 | 58 | err = cmd.Start() 59 | if err != nil { 60 | return nil, nil, err 61 | } 62 | 63 | return &groupProcess{ 64 | process: cmd.Process, 65 | processPty: processPty, 66 | }, stdin, nil 67 | } 68 | 69 | type groupProcess struct { 70 | process *os.Process 71 | processPty *os.File 72 | } 73 | 74 | func (proc *groupProcess) Signal(signal garden.Signal) error { 75 | var err error 76 | 77 | switch signal { 78 | case garden.SignalTerminate: 79 | err = proc.process.Signal(syscall.SIGTERM) 80 | default: // only other case is kill, but if we don't know it, go nuclear 81 | err = proc.process.Signal(syscall.SIGKILL) 82 | } 83 | 84 | return err 85 | } 86 | 87 | func (proc *groupProcess) Wait() (int, error) { 88 | state, err := proc.process.Wait() 89 | if err != nil { 90 | return -1, err 91 | } 92 | 93 | return state.Sys().(syscall.WaitStatus).ExitStatus(), nil 94 | } 95 | 96 | func (process *groupProcess) SetWindowSize(size garden.WindowSize) error { 97 | if process.processPty != nil { 98 | return ptyutil.SetWinSize(process.processPty, size.Columns, size.Rows) 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /process/spawn_windows.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "syscall" 11 | "time" 12 | "unicode/utf16" 13 | "unsafe" 14 | 15 | "code.cloudfoundry.org/garden" 16 | "github.com/vito/houdini/win32" 17 | ) 18 | 19 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 20 | 21 | var procTerminateJobObject = kernel32.NewProc("TerminateJobObject") 22 | 23 | func terminateJobObject(thread syscall.Handle, exitCode uint32) (err error) { 24 | if r1, _, e1 := procTerminateJobObject.Call(uintptr(thread), uintptr(exitCode)); int(r1) != 0 { 25 | return os.NewSyscallError("TerminateJobObject", e1) 26 | } 27 | return nil 28 | } 29 | 30 | func spawn(cmd *exec.Cmd, _ *garden.TTYSpec, stdout io.Writer, stderr io.Writer) (process, io.WriteCloser, error) { 31 | ro, wo, err := os.Pipe() 32 | if err != nil { 33 | return nil, nil, fmt.Errorf("pipe failed: %s", err) 34 | } 35 | 36 | re, we, err := os.Pipe() 37 | if err != nil { 38 | return nil, nil, fmt.Errorf("pipe failed: %s", err) 39 | } 40 | 41 | ri, wi, err := os.Pipe() 42 | if err != nil { 43 | return nil, nil, fmt.Errorf("pipe failed: %s", err) 44 | } 45 | 46 | go io.Copy(stdout, ro) 47 | go io.Copy(stderr, re) 48 | 49 | attr := &syscall.ProcAttr{ 50 | Dir: cmd.Dir, 51 | Env: cmd.Env, 52 | Files: []uintptr{ri.Fd(), wo.Fd(), we.Fd()}, 53 | } 54 | 55 | lookedUpPath, err := lookExtensions(cmd.Path, cmd.Dir) 56 | if err != nil { 57 | return nil, nil, fmt.Errorf("look extensions failed: %s", err) 58 | } 59 | 60 | // Acquire the fork lock so that no other threads 61 | // create new fds that are not yet close-on-exec 62 | // before we fork. 63 | syscall.ForkLock.Lock() 64 | defer syscall.ForkLock.Unlock() 65 | 66 | p, _ := syscall.GetCurrentProcess() 67 | fd := make([]syscall.Handle, len(attr.Files)) 68 | for i := range attr.Files { 69 | if attr.Files[i] > 0 { 70 | err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS) 71 | if err != nil { 72 | return nil, nil, fmt.Errorf("duplicating handle failed: %s", err) 73 | } 74 | 75 | defer syscall.CloseHandle(syscall.Handle(fd[i])) 76 | } 77 | } 78 | 79 | si := new(syscall.StartupInfo) 80 | si.Cb = uint32(unsafe.Sizeof(*si)) 81 | si.Flags = syscall.STARTF_USESTDHANDLES 82 | si.StdInput = fd[0] 83 | si.StdOutput = fd[1] 84 | si.StdErr = fd[2] 85 | 86 | pi := new(syscall.ProcessInformation) 87 | 88 | flags := uint32(syscall.CREATE_UNICODE_ENVIRONMENT) 89 | flags |= win32.CREATE_SUSPENDED 90 | flags |= win32.CREATE_BREAKAWAY_FROM_JOB 91 | 92 | argvp0, err := syscall.UTF16PtrFromString(lookedUpPath) 93 | if err != nil { 94 | return nil, nil, fmt.Errorf("stringing failed: %s", err) 95 | } 96 | 97 | argvp0v0v0v0, err := syscall.UTF16PtrFromString(makeCmdLine(cmd.Args)) 98 | if err != nil { 99 | return nil, nil, fmt.Errorf("stringing failed: %s", err) 100 | } 101 | 102 | dirp, err := syscall.UTF16PtrFromString(attr.Dir) 103 | if err != nil { 104 | return nil, nil, fmt.Errorf("stringing failed: %s", err) 105 | } 106 | 107 | err = syscall.CreateProcess( 108 | argvp0, 109 | argvp0v0v0v0, 110 | nil, 111 | nil, 112 | true, 113 | flags, 114 | createEnvBlock(attr.Env), 115 | dirp, 116 | si, 117 | pi, 118 | ) 119 | if err != nil { 120 | return nil, nil, fmt.Errorf("create process: %s", err) 121 | } 122 | 123 | ri.Close() 124 | wo.Close() 125 | we.Close() 126 | 127 | jobName, err := syscall.UTF16PtrFromString(fmt.Sprintf("%d", time.Now().UnixNano())) 128 | if err != nil { 129 | return nil, nil, fmt.Errorf("stringing failed: %s", err) 130 | } 131 | 132 | jobHandle, err := win32.CreateJobObject(nil, jobName) 133 | if err != nil { 134 | return nil, nil, fmt.Errorf("create job failed: %s", err) 135 | } 136 | 137 | err = win32.AssignProcessToJobObject(jobHandle, pi.Process) 138 | if err != nil { 139 | return nil, nil, fmt.Errorf("assign failed: %s", err) 140 | } 141 | 142 | _, err = win32.ResumeThread(pi.Thread) 143 | if err != nil { 144 | return nil, nil, fmt.Errorf("resume failed: %s", err) 145 | } 146 | 147 | return &jobProcess{ 148 | jobHandle: jobHandle, 149 | processHandle: pi.Process, 150 | }, wi, nil 151 | } 152 | 153 | type jobProcess struct { 154 | jobHandle syscall.Handle 155 | processHandle syscall.Handle 156 | } 157 | 158 | func (process *jobProcess) Signal(garden.Signal) error { 159 | return terminateJobObject(process.jobHandle, 1) 160 | } 161 | 162 | func (process *jobProcess) Wait() (int, error) { 163 | s, e := syscall.WaitForSingleObject(syscall.Handle(process.processHandle), syscall.INFINITE) 164 | switch s { 165 | case syscall.WAIT_OBJECT_0: 166 | break 167 | case syscall.WAIT_FAILED: 168 | return -1, os.NewSyscallError("WaitForSingleObject", e) 169 | default: 170 | return -1, errors.New("os: unexpected result from WaitForSingleObject") 171 | } 172 | 173 | var ec uint32 174 | e = syscall.GetExitCodeProcess(syscall.Handle(process.processHandle), &ec) 175 | if e != nil { 176 | return -1, os.NewSyscallError("GetExitCodeProcess", e) 177 | } 178 | 179 | var u syscall.Rusage 180 | e = syscall.GetProcessTimes(syscall.Handle(process.processHandle), &u.CreationTime, &u.ExitTime, &u.KernelTime, &u.UserTime) 181 | if e != nil { 182 | return -1, os.NewSyscallError("GetProcessTimes", e) 183 | } 184 | 185 | // NOTE(brainman): It seems that sometimes process is not dead 186 | // when WaitForSingleObject returns. But we do not know any 187 | // other way to wait for it. Sleeping for a while seems to do 188 | // the trick sometimes. So we will sleep and smell the roses. 189 | defer time.Sleep(5 * time.Millisecond) 190 | defer syscall.CloseHandle(syscall.Handle(process.processHandle)) 191 | 192 | return int(ec), nil 193 | } 194 | 195 | func (process *jobProcess) SetWindowSize(garden.WindowSize) error { 196 | return nil 197 | } 198 | 199 | func makeCmdLine(args []string) string { 200 | var s string 201 | for _, v := range args { 202 | if s != "" { 203 | s += " " 204 | } 205 | s += syscall.EscapeArg(v) 206 | } 207 | return s 208 | } 209 | 210 | func createEnvBlock(envv []string) *uint16 { 211 | if len(envv) == 0 { 212 | return &utf16.Encode([]rune("\x00\x00"))[0] 213 | } 214 | length := 0 215 | for _, s := range envv { 216 | length += len(s) + 1 217 | } 218 | length += 1 219 | 220 | b := make([]byte, length) 221 | i := 0 222 | for _, s := range envv { 223 | l := len(s) 224 | copy(b[i:i+l], []byte(s)) 225 | copy(b[i+l:i+l+1], []byte{0}) 226 | i = i + l + 1 227 | } 228 | copy(b[i:i+1], []byte{0}) 229 | 230 | return &utf16.Encode([]rune(string(b)))[0] 231 | } 232 | 233 | // adapted from lookExtensions but returns resolved path rather than stripping it out 234 | func lookExtensions(path, dir string) (string, error) { 235 | if filepath.Base(path) == path { 236 | path = filepath.Join(".", path) 237 | } 238 | if dir == "" { 239 | return exec.LookPath(path) 240 | } 241 | if filepath.VolumeName(path) != "" { 242 | return exec.LookPath(path) 243 | } 244 | if len(path) > 1 && os.IsPathSeparator(path[0]) { 245 | return exec.LookPath(path) 246 | } 247 | 248 | dirandpath := filepath.Join(dir, path) 249 | 250 | return exec.LookPath(dirandpath) 251 | } 252 | -------------------------------------------------------------------------------- /ptyutil/raw.go: -------------------------------------------------------------------------------- 1 | package ptyutil 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/term/termios" 7 | ) 8 | 9 | func SetRaw(tty *os.File) error { 10 | attr, err := termios.Tcgetattr(tty.Fd()) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | termios.Cfmakeraw(attr) 16 | 17 | return termios.Tcsetattr(tty.Fd(), termios.TCSANOW, attr) 18 | } 19 | -------------------------------------------------------------------------------- /ptyutil/winsize.go: -------------------------------------------------------------------------------- 1 | // +build !solaris 2 | 3 | package ptyutil 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "unsafe" 9 | ) 10 | 11 | type ttySize struct { 12 | Rows uint16 13 | Cols uint16 14 | Xpixel uint16 15 | Ypixel uint16 16 | } 17 | 18 | func SetWinSize(f *os.File, cols int, rows int) error { 19 | _, _, e := syscall.Syscall6( 20 | syscall.SYS_IOCTL, 21 | uintptr(f.Fd()), 22 | uintptr(syscall.TIOCSWINSZ), 23 | uintptr(unsafe.Pointer(&ttySize{uint16(rows), uint16(cols), 0, 0})), 24 | 0, 0, 0, 25 | ) 26 | 27 | if e != 0 { 28 | return syscall.ENOTTY 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /ptyutil/winsize_solaris.go: -------------------------------------------------------------------------------- 1 | package ptyutil 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "unsafe" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | type ttySize struct { 12 | Rows uint16 13 | Cols uint16 14 | Xpixel uint16 15 | Ypixel uint16 16 | } 17 | 18 | func SetWinSize(f *os.File, cols int, rows int) error { 19 | return unix.IoctlSetInt(int(f.Fd()), int(syscall.TIOCSWINSZ), 20 | int(uintptr(unsafe.Pointer(&ttySize{uint16(rows), uint16(cols), 0, 0})))) 21 | } 22 | -------------------------------------------------------------------------------- /win32/win32_windows.go: -------------------------------------------------------------------------------- 1 | // cribbed from github.com/contester/runlib -- it has out of date deps which 2 | // makes it hard to keep in sync 3 | 4 | package win32 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | var ( 13 | advapi32 = syscall.NewLazyDLL("advapi32.dll") 14 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 15 | psapi = syscall.NewLazyDLL("psapi.dll") 16 | userenv = syscall.NewLazyDLL("userenv.dll") 17 | user32 = syscall.NewLazyDLL("user32.dll") 18 | 19 | procCreateProcessWithLogonW = advapi32.NewProc("CreateProcessWithLogonW") 20 | procCreateProcessAsUserW = advapi32.NewProc("CreateProcessAsUserW") 21 | procResumeThread = kernel32.NewProc("ResumeThread") 22 | procGetProcessMemoryInfo = psapi.NewProc("GetProcessMemoryInfo") 23 | procLogonUserW = advapi32.NewProc("LogonUserW") 24 | procLoadUserProfileW = userenv.NewProc("LoadUserProfileW") 25 | procUnloadUserProfile = userenv.NewProc("UnloadUserProfile") 26 | procGetProcessWindowStation = user32.NewProc("GetProcessWindowStation") 27 | procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId") 28 | procGetThreadDesktop = user32.NewProc("GetThreadDesktop") 29 | procCreateWindowStationW = user32.NewProc("CreateWindowStationW") 30 | procSetProcessWindowStation = user32.NewProc("SetProcessWindowStation") 31 | procCreateDesktopW = user32.NewProc("CreateDesktopW") 32 | procSetThreadDesktop = user32.NewProc("SetThreadDesktop") 33 | procGetUserObjectInformationW = user32.NewProc("GetUserObjectInformationW") 34 | procCloseWindowStation = user32.NewProc("CloseWindowStation") 35 | procCreateJobObjectW = kernel32.NewProc("CreateJobObjectW") 36 | procQueryInformationJobObject = kernel32.NewProc("QueryInformationJobObject") 37 | procSetInformationJobObject = kernel32.NewProc("SetInformationJobObject") 38 | procAssignProcessToJobObject = kernel32.NewProc("AssignProcessToJobObject") 39 | procVirtualAllocEx = kernel32.NewProc("VirtualAllocEx") 40 | procWriteProcessMemory = kernel32.NewProc("WriteProcessMemory") 41 | procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW") 42 | procCreateRemoteThread = kernel32.NewProc("CreateRemoteThread") 43 | procVirtualFreeEx = kernel32.NewProc("VirtualFreeEx") 44 | procSetProcessAffinityMask = kernel32.NewProc("SetProcessAffinityMask") 45 | procGetProcessAffinityMask = kernel32.NewProc("GetProcessAffinityMask") 46 | procVerifyVersionInfoW = kernel32.NewProc("VerifyVersionInfoW") 47 | procVerSetConditionMask = kernel32.NewProc("VerSetConditionMask") 48 | ) 49 | 50 | const ( 51 | CREATE_BREAKAWAY_FROM_JOB = 0x01000000 52 | CREATE_NEW_CONSOLE = 0x00000010 53 | CREATE_NEW_PROCESS_GROUP = 0x00000200 54 | CREATE_SUSPENDED = 0x00000004 55 | 56 | LOGON_WITH_PROFILE = 0x00000001 57 | 58 | STARTF_FORCEOFFFEEDBACK = 0x00000080 59 | 60 | FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000 61 | 62 | LOGON32_PROVIDER_DEFAULT = 0 63 | LOGON32_PROVIDER_WINNT35 = 1 64 | LOGON32_PROVIDER_WINNT40 = 2 65 | LOGON32_PROVIDER_WINNT50 = 3 66 | 67 | LOGON32_LOGON_INTERACTIVE = 2 68 | LOGON32_LOGON_NETWORK = 3 69 | LOGON32_LOGON_BATCH = 4 70 | LOGON32_LOGON_SERVICE = 5 71 | LOGON32_LOGON_UNLOCK = 7 72 | LOGON32_LOGON_NETWORK_CLEARTEXT = 8 73 | LOGON32_LOGON_NEW_CREDENTIALS = 9 74 | 75 | MAXIMUM_ALLOWED = 0x2000000 76 | PI_NOUI = 1 77 | 78 | VER_BUILDNUMBER = 0x0000004 79 | VER_MAJORVERSION = 0x0000002 80 | VER_MINORVERSION = 0x0000001 81 | VER_PLATFORMID = 0x0000008 82 | VER_PRODUCT_TYPE = 0x0000080 83 | VER_SERVICEPACKMAJOR = 0x0000020 84 | VER_SERVICEPACKMINOR = 0x0000010 85 | VER_SUITENAME = 0x0000040 86 | 87 | VER_EQUAL = 1 88 | VER_GREATER = 2 89 | VER_GREATER_EQUAL = 3 90 | VER_LESS = 4 91 | VER_LESS_EQUAL = 5 92 | 93 | ERROR_OLD_WIN_VERSION syscall.Errno = 1150 94 | ) 95 | 96 | type OSVersionInfoEx struct { 97 | OSVersionInfoSize uint32 98 | MajorVersion uint32 99 | MinorVersion uint32 100 | BuildNumber uint32 101 | PlatformId uint32 102 | CSDVersion [128]uint16 103 | ServicePackMajor uint16 104 | ServicePackMinor uint16 105 | SuiteMask uint16 106 | ProductType byte 107 | Reserve byte 108 | } 109 | 110 | func IsWindows8OrGreater() bool { 111 | cm := VerSetConditionMask(0, VER_MAJORVERSION, VER_GREATER_EQUAL) 112 | cm = VerSetConditionMask(cm, VER_MINORVERSION, VER_GREATER_EQUAL) 113 | cm = VerSetConditionMask(cm, VER_SERVICEPACKMAJOR, VER_GREATER_EQUAL) 114 | cm = VerSetConditionMask(cm, VER_SERVICEPACKMINOR, VER_GREATER_EQUAL) 115 | r, _ := VerifyWindowsInfoW(OSVersionInfoEx{ 116 | MajorVersion: 6, 117 | MinorVersion: 2, 118 | }, VER_MAJORVERSION|VER_MINORVERSION|VER_SERVICEPACKMAJOR|VER_SERVICEPACKMINOR, cm) 119 | return r 120 | } 121 | 122 | type ProcessMemoryCountersEx struct { 123 | Cb uint32 // DWORD 124 | PageFaultCount uint32 // DWORD 125 | PeakWorkingSetSize uintptr // SIZE_T 126 | WorkingSetSize uintptr // SIZE_T 127 | QuotaPeakPagedPoolUsage uintptr // SIZE_T 128 | QuotaPagedPoolUsage uintptr // SIZE_T 129 | QuotaPeakNonPagedPoolUsage uintptr // SIZE_T 130 | QuotaNonPagedPoolUsage uintptr // SIZE_T 131 | PagefileUsage uintptr // SIZE_T 132 | PeakPagefileUsage uintptr // SIZE_T 133 | PrivateUsage uintptr // SIZE_T 134 | } 135 | 136 | type ProfileInfo struct { 137 | Size uint32 138 | Flags uint32 139 | Username *uint16 140 | ProfilePath *uint16 141 | DefaultPath *uint16 142 | ServerName *uint16 143 | PolicyPath *uint16 144 | Profile syscall.Handle 145 | } 146 | 147 | type Hwinsta uintptr 148 | type Hdesk uintptr 149 | 150 | func MakeInheritSa() *syscall.SecurityAttributes { 151 | var sa syscall.SecurityAttributes 152 | sa.Length = uint32(unsafe.Sizeof(sa)) 153 | sa.InheritHandle = 1 154 | return &sa 155 | } 156 | 157 | func StringPtrToUTF16Ptr(src *string) (result *uint16) { 158 | if src != nil { 159 | return syscall.StringToUTF16Ptr(*src) 160 | } 161 | return nil 162 | } 163 | 164 | func ListToEnvironmentBlock(list *[]string) *uint16 { 165 | if list == nil { 166 | return nil 167 | } 168 | 169 | size := 1 170 | for _, v := range *list { 171 | size += len(syscall.StringToUTF16(v)) 172 | } 173 | 174 | result := make([]uint16, size) 175 | 176 | tail := 0 177 | 178 | for _, v := range *list { 179 | uline := syscall.StringToUTF16(v) 180 | copy(result[tail:], uline) 181 | tail += len(uline) 182 | } 183 | 184 | result[tail] = 0 185 | 186 | return &result[0] 187 | } 188 | 189 | func CreateProcessWithLogonW( 190 | username *uint16, 191 | domain *uint16, 192 | password *uint16, 193 | logonFlags uint32, 194 | applicationName *uint16, 195 | commandLine *uint16, 196 | creationFlags uint32, 197 | environment *uint16, 198 | currentDirectory *uint16, 199 | startupInfo *syscall.StartupInfo, 200 | processInformation *syscall.ProcessInformation) error { 201 | if r1, _, e1 := procCreateProcessWithLogonW.Call( 202 | uintptr(unsafe.Pointer(username)), 203 | uintptr(unsafe.Pointer(domain)), 204 | uintptr(unsafe.Pointer(password)), 205 | uintptr(logonFlags), 206 | uintptr(unsafe.Pointer(applicationName)), 207 | uintptr(unsafe.Pointer(commandLine)), 208 | uintptr(creationFlags), 209 | uintptr(unsafe.Pointer(environment)), // env 210 | uintptr(unsafe.Pointer(currentDirectory)), 211 | uintptr(unsafe.Pointer(startupInfo)), 212 | uintptr(unsafe.Pointer(processInformation))); int(r1) == 0 { 213 | return os.NewSyscallError("CreateProcessWithLogonW", e1) 214 | } 215 | return nil 216 | } 217 | 218 | func boolToUint32(src bool) uint32 { 219 | if src { 220 | return 1 221 | } 222 | return 0 223 | } 224 | 225 | func CreateProcessAsUser( 226 | token syscall.Handle, 227 | applicationName *uint16, 228 | commandLine *uint16, 229 | procSecurity *syscall.SecurityAttributes, 230 | threadSecurity *syscall.SecurityAttributes, 231 | inheritHandles bool, 232 | creationFlags uint32, 233 | environment *uint16, 234 | currentDirectory *uint16, 235 | startupInfo *syscall.StartupInfo, 236 | processInformation *syscall.ProcessInformation) error { 237 | 238 | if r1, _, e1 := procCreateProcessAsUserW.Call( 239 | uintptr(token), 240 | uintptr(unsafe.Pointer(applicationName)), 241 | uintptr(unsafe.Pointer(commandLine)), 242 | uintptr(unsafe.Pointer(procSecurity)), 243 | uintptr(unsafe.Pointer(threadSecurity)), 244 | uintptr(boolToUint32(inheritHandles)), 245 | uintptr(creationFlags), 246 | uintptr(unsafe.Pointer(environment)), // env 247 | uintptr(unsafe.Pointer(currentDirectory)), 248 | uintptr(unsafe.Pointer(startupInfo)), 249 | uintptr(unsafe.Pointer(processInformation))); int(r1) == 0 { 250 | return os.NewSyscallError("CreateProcessAsUser", e1) 251 | } 252 | return nil 253 | } 254 | 255 | func ResumeThread(thread syscall.Handle) (suspendCount int, err error) { 256 | if r1, _, e1 := procResumeThread.Call(uintptr(thread)); int(r1) == -1 { 257 | return -1, os.NewSyscallError("ResumeThread", e1) 258 | } else { 259 | return int(r1), nil 260 | } 261 | } 262 | 263 | func GetProcessMemoryInfo(process syscall.Handle) (pmc *ProcessMemoryCountersEx, err error) { 264 | pmc = &ProcessMemoryCountersEx{} 265 | pmc.Cb = uint32(unsafe.Sizeof(*pmc)) 266 | if r1, _, e1 := procGetProcessMemoryInfo.Call(uintptr(process), uintptr(unsafe.Pointer(pmc)), uintptr(pmc.Cb)); int(r1) == 0 { 267 | return nil, os.NewSyscallError("GetProcessMemoryInfo", e1) 268 | } 269 | return pmc, nil 270 | } 271 | 272 | func LogonUser(username *uint16, domain *uint16, password *uint16, logonType uint32, logonProvider uint32) (token syscall.Handle, err error) { 273 | if r1, _, e1 := procLogonUserW.Call( 274 | uintptr(unsafe.Pointer(username)), 275 | uintptr(unsafe.Pointer(domain)), 276 | uintptr(unsafe.Pointer(password)), 277 | uintptr(logonType), 278 | uintptr(logonProvider), 279 | uintptr(unsafe.Pointer(&token))); int(r1) == 0 { 280 | return syscall.InvalidHandle, os.NewSyscallError("LogonUser", e1) 281 | } 282 | return 283 | } 284 | 285 | func LoadUserProfile(token syscall.Handle, pinfo *ProfileInfo) error { 286 | if r1, _, e1 := procLoadUserProfileW.Call( 287 | uintptr(token), 288 | uintptr(unsafe.Pointer(pinfo))); int(r1) == 0 { 289 | return os.NewSyscallError("LoadUserProfile", e1) 290 | } 291 | return nil 292 | } 293 | 294 | func UnloadUserProfile(token, profile syscall.Handle) error { 295 | if r1, _, e1 := procUnloadUserProfile.Call( 296 | uintptr(token), 297 | uintptr(profile)); int(r1) == 0 { 298 | return os.NewSyscallError("UnloadUserProfile", e1) 299 | } 300 | return nil 301 | } 302 | 303 | func GetProcessWindowStation() (Hwinsta, error) { 304 | r1, _, e1 := procGetProcessWindowStation.Call() 305 | if int(r1) == 0 { 306 | return Hwinsta(r1), os.NewSyscallError("GetProcessWindowStation", e1) 307 | } 308 | return Hwinsta(r1), nil 309 | } 310 | 311 | func GetCurrentThreadId() uint32 { 312 | r1, _, _ := procGetCurrentThreadId.Call() 313 | return uint32(r1) 314 | } 315 | 316 | func GetThreadDesktop(threadId uint32) (Hdesk, error) { 317 | r1, _, e1 := procGetThreadDesktop.Call( 318 | uintptr(threadId)) 319 | if int(r1) == 0 { 320 | return Hdesk(r1), os.NewSyscallError("GetThreadDesktop", e1) 321 | } 322 | return Hdesk(r1), nil 323 | } 324 | 325 | func CreateWindowStation(winsta *uint16, flags, desiredAccess uint32, sa *syscall.SecurityAttributes) (Hwinsta, error) { 326 | r1, _, e1 := procCreateWindowStationW.Call( 327 | uintptr(unsafe.Pointer(winsta)), 328 | uintptr(flags), 329 | uintptr(desiredAccess), 330 | uintptr(unsafe.Pointer(sa))) 331 | if int(r1) == 0 { 332 | return Hwinsta(r1), os.NewSyscallError("CreateWindowStation", e1) 333 | } 334 | return Hwinsta(r1), nil 335 | } 336 | 337 | func SetProcessWindowStation(winsta Hwinsta) error { 338 | r1, _, e1 := procSetProcessWindowStation.Call( 339 | uintptr(winsta)) 340 | if int(r1) == 0 { 341 | return os.NewSyscallError("SetProcessWindowStation", e1) 342 | } 343 | return nil 344 | } 345 | 346 | func CreateDesktop(desktop, device *uint16, devmode uintptr, flags, desiredAccess uint32, sa *syscall.SecurityAttributes) (Hdesk, error) { 347 | r1, _, e1 := procCreateDesktopW.Call( 348 | uintptr(unsafe.Pointer(desktop)), 349 | uintptr(unsafe.Pointer(device)), 350 | devmode, 351 | uintptr(flags), 352 | uintptr(desiredAccess), 353 | uintptr(unsafe.Pointer(sa))) 354 | if int(r1) == 0 { 355 | return Hdesk(r1), os.NewSyscallError("CreateDesktop", e1) 356 | } 357 | return Hdesk(r1), nil 358 | } 359 | 360 | func SetThreadDesktop(desk Hdesk) error { 361 | r1, _, e1 := procSetThreadDesktop.Call( 362 | uintptr(desk)) 363 | if int(r1) == 0 { 364 | return os.NewSyscallError("SetThreadDesktop", e1) 365 | } 366 | return nil 367 | } 368 | 369 | const ( 370 | UOI_NAME = 2 371 | ) 372 | 373 | func GetUserObjectInformation(obj syscall.Handle, index int, info unsafe.Pointer, length uint32) (uint32, error) { 374 | var nLength uint32 375 | r1, _, e1 := procGetUserObjectInformationW.Call( 376 | uintptr(obj), 377 | uintptr(index), 378 | uintptr(info), 379 | uintptr(length), 380 | uintptr(unsafe.Pointer(&nLength))) 381 | if int(r1) == 0 { 382 | return nLength, os.NewSyscallError("GetUserObjectInformation", e1) 383 | } 384 | return 0, nil 385 | } 386 | 387 | func GetUserObjectName(obj syscall.Handle) (string, error) { 388 | namebuf := make([]uint16, 256) 389 | _, err := GetUserObjectInformation(obj, UOI_NAME, unsafe.Pointer(&namebuf[0]), 256*2) 390 | if err != nil { 391 | return "", err 392 | } 393 | return syscall.UTF16ToString(namebuf), nil 394 | } 395 | 396 | func CloseWindowStation(winsta Hwinsta) error { 397 | r1, _, e1 := procCloseWindowStation.Call( 398 | uintptr(winsta)) 399 | if int(r1) == 0 { 400 | return os.NewSyscallError("CloseWindowStation", e1) 401 | } 402 | return nil 403 | } 404 | 405 | func CreateJobObject(sa *syscall.SecurityAttributes, name *uint16) (syscall.Handle, error) { 406 | r1, _, e1 := procCreateJobObjectW.Call( 407 | uintptr(unsafe.Pointer(sa)), 408 | uintptr(unsafe.Pointer(name))) 409 | if int(r1) == 0 { 410 | return syscall.InvalidHandle, os.NewSyscallError("CreateJobObject", e1) 411 | } 412 | return syscall.Handle(r1), nil 413 | } 414 | 415 | func QueryInformationJobObject(job syscall.Handle, infoclass uint32, info unsafe.Pointer, length uint32) (uint32, error) { 416 | var nLength uint32 417 | r1, _, e1 := procQueryInformationJobObject.Call( 418 | uintptr(job), 419 | uintptr(infoclass), 420 | uintptr(info), 421 | uintptr(length), 422 | uintptr(unsafe.Pointer(&nLength))) 423 | 424 | if int(r1) == 0 { 425 | return nLength, os.NewSyscallError("QueryInformationJobObject", e1) 426 | } 427 | return nLength, nil 428 | } 429 | 430 | func SetInformationJobObject(job syscall.Handle, infoclass uint32, info unsafe.Pointer, length uint32) error { 431 | r1, _, e1 := procSetInformationJobObject.Call( 432 | uintptr(job), 433 | uintptr(infoclass), 434 | uintptr(info), 435 | uintptr(length)) 436 | 437 | if int(r1) == 0 { 438 | return os.NewSyscallError("SetInformationJobObject", e1) 439 | } 440 | return nil 441 | } 442 | 443 | type JobObjectBasicAccountingInformation struct { 444 | TotalUserTime uint64 445 | TotalKernelTime uint64 446 | ThisPeriodTotalUserTime uint64 447 | ThisPeriodTotalKernelTime uint64 448 | TotalPageFaultCount uint32 449 | TotalProcesses uint32 450 | ActiveProcesses uint32 451 | TotalTerminatedProcesses uint32 452 | } 453 | 454 | type JobObjectBasicUiRestrictions struct { 455 | UIRestrictionClass uint32 456 | } 457 | 458 | const ( 459 | JOB_OBJECT_UILIMIT_DESKTOP = 0x40 460 | JOB_OBJECT_UILIMIT_DISPLAYSETTINGS = 0x10 461 | JOB_OBJECT_UILIMIT_EXITWINDOWS = 0x80 462 | JOB_OBJECT_UILIMIT_GLOBALATOMS = 0x20 463 | JOB_OBJECT_UILIMIT_HANDLES = 1 464 | JOB_OBJECT_UILIMIT_READCLIPBOARD = 2 465 | JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS = 8 466 | JOB_OBJECT_UILIMIT_WRITECLIPBOARD = 4 467 | ) 468 | 469 | func GetJobObjectBasicAccountingInformation(job syscall.Handle) (*JobObjectBasicAccountingInformation, error) { 470 | var jinfo JobObjectBasicAccountingInformation 471 | _, err := QueryInformationJobObject(job, 1, unsafe.Pointer(&jinfo), uint32(unsafe.Sizeof(jinfo))) 472 | if err != nil { 473 | return nil, err 474 | } 475 | return &jinfo, nil 476 | } 477 | 478 | type JobObjectBasicLimitInformation struct { 479 | PerProcessUserTimeLimit uint64 // LARGE_INTEGER 480 | PerJobUserTimeLimit uint64 // LARGE_INTEGER 481 | LimitFlags uint32 // DWORD 482 | MinimumWorkingSetSize uintptr // SIZE_T 483 | MaximumWorkingSetSize uintptr // SIZE_T 484 | ActiveProcessLimit uint32 // DWORD 485 | Affinity uintptr // ULONG_PTR 486 | PriorityClass uint32 // DWORD 487 | SchedulingClass uint32 // DWORD 488 | } 489 | 490 | const ( 491 | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 492 | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x400 493 | JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 8 494 | JOB_OBJECT_LIMIT_JOB_MEMORY = 0x200 495 | JOB_OBJECT_LIMIT_JOB_TIME = 4 496 | JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x100 497 | JOB_OBJECT_LIMIT_PROCESS_TIME = 2 498 | JOB_OBJECT_LIMIT_WORKINGSET = 1 499 | JOB_OBJECT_LIMIT_AFFINITY = 0x00000010 500 | ) 501 | 502 | type IoCounters struct { 503 | ReadOperationCount uint64 // ULONGLONG 504 | WriteOperationCount uint64 // ULONGLONG 505 | OtherOperationCount uint64 // ULONGLONG 506 | ReadTransferCount uint64 // ULONGLONG 507 | WriteTransferCount uint64 // ULONGLONG 508 | OtherTransferCount uint64 // ULONGLONG 509 | } 510 | 511 | func GetJobObjectExtendedLimitInformation(job syscall.Handle) (*JobObjectExtendedLimitInformation, error) { 512 | var jinfo JobObjectExtendedLimitInformation 513 | _, err := QueryInformationJobObject(job, 9, unsafe.Pointer(&jinfo), uint32(unsafe.Sizeof(jinfo))) 514 | if err != nil { 515 | return nil, err 516 | } 517 | return &jinfo, nil 518 | } 519 | 520 | func SetJobObjectBasicUiRestrictions(job syscall.Handle, info *JobObjectBasicUiRestrictions) error { 521 | return SetInformationJobObject(job, 4, unsafe.Pointer(info), uint32(unsafe.Sizeof(*info))) 522 | } 523 | 524 | func SetJobObjectExtendedLimitInformation(job syscall.Handle, info *JobObjectExtendedLimitInformation) error { 525 | return SetInformationJobObject(job, 9, unsafe.Pointer(info), uint32(unsafe.Sizeof(*info))) 526 | } 527 | 528 | func AssignProcessToJobObject(job syscall.Handle, process syscall.Handle) error { 529 | r1, _, e1 := procAssignProcessToJobObject.Call( 530 | uintptr(job), 531 | uintptr(process)) 532 | if int(r1) == 0 { 533 | return os.NewSyscallError("AssignProcessToJobObject", e1) 534 | } 535 | return nil 536 | } 537 | 538 | const ( 539 | MEM_COMMIT = 0x00001000 540 | PAGE_READWRITE = 0x04 541 | ) 542 | 543 | func VirtualAllocEx(process syscall.Handle, addr uintptr, size, allocType, protect uint32) (uintptr, error) { 544 | r1, _, e1 := procVirtualAllocEx.Call( 545 | uintptr(process), 546 | addr, 547 | uintptr(size), 548 | uintptr(allocType), 549 | uintptr(protect)) 550 | 551 | if int(r1) == 0 { 552 | return r1, os.NewSyscallError("VirtualAllocEx", e1) 553 | } 554 | return r1, nil 555 | } 556 | 557 | func WriteProcessMemory(process syscall.Handle, addr uintptr, buf unsafe.Pointer, size uint32) (uint32, error) { 558 | var nLength uint32 559 | r1, _, e1 := procWriteProcessMemory.Call( 560 | uintptr(process), 561 | addr, 562 | uintptr(buf), 563 | uintptr(size), 564 | uintptr(unsafe.Pointer(&nLength))) 565 | 566 | if int(r1) == 0 { 567 | return nLength, os.NewSyscallError("WriteProcessMemory", e1) 568 | } 569 | return nLength, nil 570 | } 571 | 572 | func GetModuleHandle(name *uint16) (syscall.Handle, error) { 573 | r1, _, e1 := procGetModuleHandleW.Call(uintptr(unsafe.Pointer(name))) 574 | if int(r1) == 0 { 575 | return syscall.InvalidHandle, os.NewSyscallError("GetModuleHandle", e1) 576 | } 577 | return syscall.Handle(r1), nil 578 | } 579 | 580 | func CreateRemoteThread(process syscall.Handle, sa *syscall.SecurityAttributes, stackSize uint32, startAddress, 581 | parameter uintptr, creationFlags uint32) (syscall.Handle, uint32, error) { 582 | var threadId uint32 583 | r1, _, e1 := procCreateRemoteThread.Call( 584 | uintptr(process), 585 | uintptr(unsafe.Pointer(sa)), 586 | uintptr(stackSize), 587 | startAddress, 588 | parameter, 589 | uintptr(creationFlags), 590 | uintptr(unsafe.Pointer(&threadId))) 591 | 592 | if int(r1) == 0 { 593 | return syscall.InvalidHandle, 0, os.NewSyscallError("CreateRemoteThread", e1) 594 | } 595 | return syscall.Handle(r1), threadId, nil 596 | } 597 | 598 | const ( 599 | MEM_RELEASE = 0x8000 600 | ) 601 | 602 | func VirtualFreeEx(process syscall.Handle, addr uintptr, size, freeType uint32) error { 603 | r1, _, e1 := procVirtualFreeEx.Call( 604 | uintptr(process), 605 | addr, 606 | uintptr(size), 607 | uintptr(freeType)) 608 | 609 | if int(r1) == 0 { 610 | return os.NewSyscallError("VirtualFreeEx", e1) 611 | } 612 | return nil 613 | } 614 | 615 | func SetInheritHandle(h syscall.Handle, inherit bool) error { 616 | var v uint32 617 | if inherit { 618 | v = syscall.HANDLE_FLAG_INHERIT 619 | } 620 | return os.NewSyscallError("SetHandleInformation", 621 | syscall.SetHandleInformation(syscall.Handle(h), syscall.HANDLE_FLAG_INHERIT, v)) 622 | } 623 | 624 | func SetProcessAffinityMask(process syscall.Handle, mask uint64) error { 625 | r1, _, e1 := procSetProcessAffinityMask.Call( 626 | uintptr(process), 627 | uintptr(mask)) 628 | 629 | if int(r1) == 0 { 630 | return os.NewSyscallError("SetProcessAffinityMask", e1) 631 | } 632 | return nil 633 | } 634 | 635 | func GetProcessAffinityMask(process syscall.Handle) (processMask, systemMask uint64, err error) { 636 | r1, _, e1 := procGetProcessAffinityMask.Call( 637 | uintptr(process), 638 | uintptr(unsafe.Pointer(&processMask)), 639 | uintptr(unsafe.Pointer(&systemMask))) 640 | 641 | if int(r1) == 0 { 642 | err = os.NewSyscallError("GetProcessAffinityMask", e1) 643 | } 644 | return 645 | } 646 | -------------------------------------------------------------------------------- /win32/win32_windows_386.go: -------------------------------------------------------------------------------- 1 | package win32 2 | import ( 3 | "os" 4 | "unsafe") 5 | 6 | type JobObjectExtendedLimitInformation struct { 7 | BasicLimitInformation JobObjectBasicLimitInformation 8 | align1 uint32 9 | IoInfo IoCounters 10 | ProcessMemoryLimit uintptr // SIZE_T 11 | JobMemoryLimit uintptr // SIZE_T 12 | PeakProcessMemoryUsed uintptr // SIZE_T 13 | PeakJobMemoryUsed uintptr // SIZE_T 14 | } 15 | 16 | func unpackConditionMask(cm uint64) (m1, m2 uintptr) { 17 | return uintptr(cm & 0xffffffff), uintptr(cm >> 32) 18 | } 19 | 20 | func packConditionMask(m1, m2 uintptr) uint64 { 21 | return uint64(m1) | (uint64(m2) << 32) 22 | } 23 | 24 | func VerSetConditionMask(lConditionMask uint64, typeBitMask uint32, conditionMask uint8) uint64 { 25 | m1, m2 := unpackConditionMask(lConditionMask) 26 | 27 | r1, r2, _ := procVerSetConditionMask.Call(m1, m2, uintptr(typeBitMask), uintptr(conditionMask)) 28 | return packConditionMask(r1, r2) 29 | } 30 | 31 | func VerifyWindowsInfoW(vi OSVersionInfoEx, typeMask uint32, conditionMask uint64) (bool, error) { 32 | m1, m2 := unpackConditionMask(conditionMask) 33 | vi.OSVersionInfoSize = uint32(unsafe.Sizeof(vi)) 34 | 35 | r1, _, e1 := procVerifyVersionInfoW.Call(uintptr(unsafe.Pointer(&vi)), uintptr(typeMask), m1, m2) 36 | if r1 != 0 { 37 | return true, nil 38 | } 39 | if r1 == 0 && e1 == ERROR_OLD_WIN_VERSION { 40 | return false, nil 41 | } 42 | return false, os.NewSyscallError("VerifyVersionInfoW", e1) 43 | } 44 | -------------------------------------------------------------------------------- /win32/win32_windows_amd64.go: -------------------------------------------------------------------------------- 1 | package win32 2 | import ( 3 | "unsafe" 4 | "os") 5 | 6 | type JobObjectExtendedLimitInformation struct { 7 | BasicLimitInformation JobObjectBasicLimitInformation 8 | IoInfo IoCounters 9 | ProcessMemoryLimit uintptr // SIZE_T 10 | JobMemoryLimit uintptr // SIZE_T 11 | PeakProcessMemoryUsed uintptr // SIZE_T 12 | PeakJobMemoryUsed uintptr // SIZE_T 13 | } 14 | 15 | func VerSetConditionMask(lConditionMask uint64, typeBitMask uint32, conditionMask uint8) uint64 { 16 | r1, _, _ := procVerSetConditionMask.Call(uintptr(lConditionMask), uintptr(typeBitMask), uintptr(conditionMask)) 17 | return uint64(r1) 18 | } 19 | 20 | func VerifyWindowsInfoW(vi OSVersionInfoEx, typeMask uint32, conditionMask uint64) (bool, error) { 21 | vi.OSVersionInfoSize = uint32(unsafe.Sizeof(vi)) 22 | 23 | r1, _, e1 := procVerifyVersionInfoW.Call(uintptr(unsafe.Pointer(&vi)), uintptr(typeMask), uintptr(conditionMask)) 24 | if r1 != 0 { 25 | return true, nil 26 | } 27 | if r1 == 0 && e1 == ERROR_OLD_WIN_VERSION { 28 | return false, nil 29 | } 30 | return false, os.NewSyscallError("VerifyVersionInfoW", e1) 31 | } 32 | --------------------------------------------------------------------------------