create and use install/run dirs on windows
diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WindowsYamlLiveTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WindowsYamlLiveTest.java
index 6e1a0a1..974f9f6 100644
--- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WindowsYamlLiveTest.java
+++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/WindowsYamlLiveTest.java
@@ -38,6 +38,7 @@
 import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
 import org.apache.brooklyn.test.Asserts;
 import org.apache.brooklyn.util.core.task.TaskPredicates;
+import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes;
 import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
 import org.apache.brooklyn.util.text.StringPredicates;
 import org.apache.brooklyn.util.text.Strings;
@@ -151,13 +152,13 @@
     public void testPowershellMinimalist() throws Exception {
         Map<String, String> cmds = ImmutableMap.<String, String>builder()
                 .put("myarg", "myval")
-                .put("launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
-                .put("checkRunning.powershell.command", "\"& c:\\\\exit0.bat\"")
+                .put("launch.powershell.command", JavaStringEscapes.wrapJavaString("& \"$Env:INSTALL_DIR\\exit0.ps1\""))
+                .put("checkRunning.powershell.command", JavaStringEscapes.wrapJavaString("& \"$Env:INSTALL_DIR\\exit0.bat\""))
                 .build();
         
         Map<String, List<String>> stdouts = ImmutableMap.of();
         
-        runWindowsApp(cmds, stdouts, null);
+        runWindowsApp(cmds, stdouts, true, null);
     }
     
     @Test(groups="Live")
@@ -182,7 +183,7 @@
                 .put("winrm: pre-launch-command.*", ImmutableList.of("myval"))
                 .build();
         
-        runWindowsApp(cmds, stdouts, null);
+        runWindowsApp(cmds, stdouts, false, null);
     }
     
     @Test(groups="Live")
@@ -207,7 +208,7 @@
                 .put("winrm: pre-launch-command.*", ImmutableList.of("myval"))
                 .build();
         
-        runWindowsApp(cmds, stdouts, null);
+        runWindowsApp(cmds, stdouts, false, null);
     }
     
     @Test(groups="Live")
@@ -227,7 +228,7 @@
         
         Map<String, List<String>> stdouts = ImmutableMap.of();
         
-        runWindowsApp(cmds, stdouts, "winrm: pre-install-command.*");
+        runWindowsApp(cmds, stdouts, false, "winrm: pre-install-command.*");
     }
     
     // FIXME Failing to match the expected exception, but looks fine! Needs more investigation.
@@ -248,7 +249,7 @@
         
         Map<String, List<String>> stdouts = ImmutableMap.of();
         
-        runWindowsApp(cmds, stdouts, "winrm: is-running-command.*");
+        runWindowsApp(cmds, stdouts, false, "winrm: is-running-command.*");
     }
     
     // FIXME Needs more work to get the stop's task that failed, so can assert got the right error message
@@ -269,29 +270,29 @@
         
         Map<String, List<String>> stdouts = ImmutableMap.of();
         
-        runWindowsApp(cmds, stdouts, "winrm: stop-command.*");
+        runWindowsApp(cmds, stdouts, false, "winrm: stop-command.*");
     }
     
-    protected void runWindowsApp(Map<String, String> commands, Map<String, List<String>> stdouts, String taskRegexFailed) throws Exception {
+    protected void runWindowsApp(Map<String, String> commands, Map<String, List<String>> stdouts, boolean useInstallDir, String taskRegexFailed) throws Exception {
         String cmdFailed = (taskRegexFailed == null) ? null : TASK_REGEX_TO_COMMAND.get(taskRegexFailed);
         
         List<String> yaml = Lists.newArrayList();
         yaml.addAll(yamlLocation);
+        String prefix = useInstallDir ? "" : "c:\\";
         yaml.addAll(ImmutableList.of(
                 "services:",
                 "- type: org.apache.brooklyn.entity.software.base.VanillaWindowsProcess",
                 "  brooklyn.config:",
-                "    onbox.base.dir.skipResolution: true",
                 "    templates.preinstall:",
                 "      classpath://org/apache/brooklyn/camp/brooklyn/echoFreemarkerMyarg.bat: c:\\echoFreemarkerMyarg.bat",
                 "      classpath://org/apache/brooklyn/camp/brooklyn/echoFreemarkerMyarg.ps1: c:\\echoFreemarkerMyarg.ps1",
                 "    files.preinstall:",
-                "      classpath://org/apache/brooklyn/camp/brooklyn/echoArg.bat: c:\\echoArg.bat",
-                "      classpath://org/apache/brooklyn/camp/brooklyn/echoMyArg.ps1: c:\\echoMyArg.ps1",
-                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.bat: c:\\exit0.bat",
-                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.bat: c:\\exit1.bat",
-                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.ps1: c:\\exit0.ps1",
-                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.ps1: c:\\exit1.ps1",
+                "      classpath://org/apache/brooklyn/camp/brooklyn/echoArg.bat: "+prefix+"echoArg.bat",
+                "      classpath://org/apache/brooklyn/camp/brooklyn/echoMyArg.ps1: "+prefix+"echoMyArg.ps1",
+                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.bat: "+prefix+"exit0.bat",
+                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.bat: "+prefix+"exit1.bat",
+                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.ps1: "+prefix+"exit0.ps1",
+                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.ps1: "+prefix+"exit1.ps1",
                 ""));
         
         for (Map.Entry<String, String> entry : commands.entrySet()) {
@@ -363,7 +364,7 @@
     
     private void assertPhaseStreamEquals(Entity entity, String phase, String stream, Predicate<String> check) {
         Optional<Task<?>> t = findTaskOrSubTask(entity, TaskPredicates.displayNameSatisfies(StringPredicates.startsWith("winrm: "+phase)));
-        Asserts.assertThat(BrooklynTaskTags.stream(t.get(), stream).getStreamContentsAbbreviated().trim(), check);
+        Asserts.assertThat(BrooklynTaskTags.stream(t.get(), stream).streamContents.get().trim(), check);
     }
 
     @Override
diff --git a/core/src/main/java/org/apache/brooklyn/location/ssh/CanResolveOnBoxDir.java b/core/src/main/java/org/apache/brooklyn/location/ssh/CanResolveOnBoxDir.java
new file mode 100644
index 0000000..a53a319
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/location/ssh/CanResolveOnBoxDir.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.brooklyn.location.ssh;
+
+import org.apache.brooklyn.api.entity.Entity;
+
+public interface CanResolveOnBoxDir {
+
+    String resolveOnBoxDirFor(Entity entity, String unresolvedPath);
+
+}
diff --git a/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java b/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
index 909cc2b..ada35d0 100644
--- a/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
+++ b/core/src/main/java/org/apache/brooklyn/location/ssh/SshMachineLocation.java
@@ -44,6 +44,7 @@
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.MachineDetails;
 import org.apache.brooklyn.api.location.MachineLocation;
 import org.apache.brooklyn.api.location.PortRange;
@@ -57,6 +58,7 @@
 import org.apache.brooklyn.core.config.ConfigUtils;
 import org.apache.brooklyn.core.config.MapConfigKey;
 import org.apache.brooklyn.core.config.Sanitizer;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
 import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
 import org.apache.brooklyn.core.location.AbstractMachineLocation;
 import org.apache.brooklyn.core.location.BasicMachineDetails;
@@ -80,8 +82,10 @@
 import org.apache.brooklyn.util.core.internal.ssh.SshTool;
 import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool;
 import org.apache.brooklyn.util.core.mutex.WithMutexes;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.core.task.ScheduledTask;
 import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
 import org.apache.brooklyn.util.core.task.system.internal.ExecWithLoggingHelpers;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.KeyTransformingLoadingCache.KeyTransformingSameTypeLoadingCache;
@@ -128,7 +132,7 @@
  * Additionally there are routines to copyTo, copyFrom; and installTo (which tries a curl, and falls back to copyTo
  * in event the source is accessible by the caller only).
  */
-public class SshMachineLocation extends AbstractMachineLocation implements MachineLocation, PortSupplier, WithMutexes, Closeable {
+public class SshMachineLocation extends AbstractMachineLocation implements MachineLocation, PortSupplier, WithMutexes, Closeable, CanResolveOnBoxDir {
 
     private static final Logger LOG = LoggerFactory.getLogger(SshMachineLocation.class);
     private static final Logger logSsh = LoggerFactory.getLogger(BrooklynLogging.SSH_IO);
@@ -1040,4 +1044,21 @@
         return mutexes().hasMutex(mutexId);
     }
 
+    @Override
+    public String resolveOnBoxDirFor(Entity entity, String unresolvedPath) {
+        ProcessTaskWrapper<Integer> baseTask = SshEffectorTasks.ssh(
+            BashCommands.alternatives("mkdir -p \"${BASE_DIR}\"",
+                BashCommands.chain(
+                    BashCommands.sudo("mkdir -p \"${BASE_DIR}\""),
+                    BashCommands.sudo("chown "+getUser()+" \"${BASE_DIR}\""))),
+            "cd ~",
+            "cd ${BASE_DIR}",
+            "echo BASE_DIR_RESULT':'`pwd`:BASE_DIR_RESULT")
+            .environmentVariable("BASE_DIR", unresolvedPath)
+            .requiringExitCodeZero()
+            .summary("initializing on-box base dir "+unresolvedPath).newTask();
+        DynamicTasks.queueIfPossible(baseTask).orSubmitAsync(entity);
+        return Strings.getFragmentBetween(baseTask.block().getStdout(), "BASE_DIR_RESULT:", ":BASE_DIR_RESULT");
+    }
+
 }
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java
index b7748df..f945aeb 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessDriver.java
@@ -47,6 +47,7 @@
 import org.apache.brooklyn.core.entity.EntityInternal;
 import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
 import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
+import org.apache.brooklyn.core.feed.ConfigToAttributes;
 import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks;
 import org.apache.brooklyn.entity.software.base.lifecycle.MachineLifecycleEffectorTasks.CloseableLatch;
 import org.apache.brooklyn.util.collections.MutableMap;
@@ -58,6 +59,7 @@
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.stream.ReaderInputStream;
+import org.apache.brooklyn.util.text.Strings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -77,6 +79,13 @@
     protected final ResourceUtils resource;
     protected final Location location;
 
+    // we cache these for efficiency and in case the entity becomes unmanaged
+    private volatile String installDir;
+    private volatile String runDir;
+    private volatile String expandedInstallDir;
+
+    private final Object installDirSetupMutex = new Object();
+
     public AbstractSoftwareProcessDriver(EntityLocal entity, Location location) {
         this.entity = checkNotNull(entity, "entity");
         this.location = checkNotNull(location, "location");
@@ -441,6 +450,10 @@
         }
     }
 
+    protected String mergePaths(String ...s) {
+        return Os.mergePathsUnix(s);
+    }
+    
     private void applyFnToResourcesAppendToList(
             Map<String, String> resources, final Function<SourceAndDestination, Task<?>> function,
             String destinationParentDir, final List<TaskAdaptable<?>> tasks) {
@@ -448,7 +461,7 @@
         for (Map.Entry<String, String> entry : resources.entrySet()) {
             final String source = checkNotNull(entry.getKey(), "Missing source for resource");
             String target = checkNotNull(entry.getValue(), "Missing destination for resource");
-            final String destination = Os.isAbsolutish(target) ? target : Os.mergePathsUnix(destinationParentDir, target);
+            final String destination = Os.isAbsolutish(target) ? target : mergePaths(destinationParentDir, target);
 
             // if source is a directory then copy all files underneath.
             // e.g. /tmp/a/{b,c/d}, source = /tmp/a, destination = dir/a/b and dir/a/c/d.
@@ -463,7 +476,7 @@
                         public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                             if (attrs.isRegularFile()) {
                                 Path relativePath = file.subpath(startElements, file.getNameCount());
-                                tasks.add(function.apply(new SourceAndDestination(file.toString(), Os.mergePathsUnix(destination, relativePath.toString()))));
+                                tasks.add(function.apply(new SourceAndDestination(file.toString(), mergePaths(destination, relativePath.toString()))));
                             }
                             return FileVisitResult.CONTINUE;
                         }
@@ -562,18 +575,19 @@
     }
 
     /**
-     * @param template URI of file to template and copy, e.g. file://.., http://.., classpath://..
+     * @param templateUrl URI of file to template and copy, e.g. file://.., http://.., classpath://..
      * @param target Destination on server.
      * @param extraSubstitutions Extra substitutions for the templater to use, for example
      *               "foo" -> "bar", and in a template ${foo}.
      * @return The exit code of the SSH command run.
      */
-    public int copyTemplate(String template, String target, boolean createParent, Map<String, ?> extraSubstitutions) {
-        String data = processTemplate(template, extraSubstitutions);
+    public int copyTemplate(String templateUrl, String target, boolean createParent, Map<String, ?> extraSubstitutions) {
+        log.debug("Processing template "+templateUrl+" and copying to "+target+" on "+getLocation()+" for "+getEntity());
+        String data = processTemplate(templateUrl, extraSubstitutions);
         return copyResource(MutableMap.<Object,Object>of(), new StringReader(data), target, createParent);
     }
 
-    public abstract int copyResource(Map<Object,Object> sshFlags, String source, String target, boolean createParentDir);
+    public abstract int copyResource(Map<Object,Object> sshFlags, String sourceUrl, String target, boolean createParentDir);
 
     public abstract int copyResource(Map<Object,Object> sshFlags, InputStream source, String target, boolean createParentDir);
 
@@ -595,8 +609,8 @@
         return copyResource(MutableMap.of(), resource, target);
     }
 
-    public int copyResource(String resource, String target, boolean createParentDir) {
-        return copyResource(MutableMap.of(), resource, target, createParentDir);
+    public int copyResource(String resourceUrl, String target, boolean createParentDir) {
+        return copyResource(MutableMap.of(), resourceUrl, target, createParentDir);
     }
 
     @SuppressWarnings({ "rawtypes", "unchecked" })
@@ -677,6 +691,95 @@
         return envSerializer.serialize(env);
     }
 
-    public abstract String getRunDir();
-    public abstract String getInstallDir();
+
+    protected void setInstallDir(String installDir) {
+        this.installDir = installDir;
+        entity.sensors().set(SoftwareProcess.INSTALL_DIR, installDir);
+    }
+
+    public String getInstallDir() {
+        if (installDir != null) return installDir;
+
+        String existingVal = getEntity().getAttribute(SoftwareProcess.INSTALL_DIR);
+        if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
+            installDir = existingVal;
+            return installDir;
+        }
+
+        synchronized (installDirSetupMutex) {
+            // previously we looked at sensor value, but we shouldn't as it might have been converted from the config key value
+            // *before* we computed the install label, or that label may have changed since previous install; now force a recompute
+            setInstallLabel();
+
+            // set it null first so that we force a recompute
+            setInstallDir(null);
+            setInstallDir(Os.tidyPath(ConfigToAttributes.apply(getEntity(), SoftwareProcess.INSTALL_DIR)));
+            return installDir;
+        }
+    }
+
+    protected void setInstallLabel() {
+        if (((EntityInternal)getEntity()).config().getLocalRaw(SoftwareProcess.INSTALL_UNIQUE_LABEL).isPresentAndNonNull()) return;
+        getEntity().config().set(SoftwareProcess.INSTALL_UNIQUE_LABEL,
+            getEntity().getEntityType().getSimpleName()+
+            (Strings.isNonBlank(getVersion()) ? "_"+getVersion() : "")+
+            (Strings.isNonBlank(getInstallLabelExtraSalt()) ? "_"+getInstallLabelExtraSalt() : "") );
+    }
+
+    /** allows subclasses to return extra salt (ie unique hash)
+     * for cases where install dirs need to be distinct e.g. based on extra plugins being placed in the install dir;
+     * {@link #setInstallLabel()} uses entity-type simple name and version already
+     * <p>
+     * this salt should not be too long and must not contain invalid path chars.
+     * a hash code of other relevant info is not a bad choice.
+     **/
+    protected String getInstallLabelExtraSalt() {
+        return null;
+    }
+
+    protected void setRunDir(String runDir) {
+        this.runDir = runDir;
+        entity.sensors().set(SoftwareProcess.RUN_DIR, runDir);
+    }
+
+    public String getRunDir() {
+        if (runDir != null) return runDir;
+
+        String existingVal = getEntity().getAttribute(SoftwareProcess.RUN_DIR);
+        if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
+            runDir = existingVal;
+            return runDir;
+        }
+
+        setRunDir(Os.tidyPath(ConfigToAttributes.apply(getEntity(), SoftwareProcess.RUN_DIR)));
+        return runDir;
+    }
+
+    public void setExpandedInstallDir(String val) {
+        String oldVal = getEntity().getAttribute(SoftwareProcess.EXPANDED_INSTALL_DIR);
+        if (Strings.isNonBlank(oldVal) && !oldVal.equals(val)) {
+            log.info("Resetting expandedInstallDir (to "+val+" from "+oldVal+") for "+getEntity());
+        }
+
+        expandedInstallDir = val;
+        getEntity().sensors().set(SoftwareProcess.EXPANDED_INSTALL_DIR, val);
+    }
+
+    public String getExpandedInstallDir() {
+        if (expandedInstallDir != null) return expandedInstallDir;
+
+        String existingVal = getEntity().getAttribute(SoftwareProcess.EXPANDED_INSTALL_DIR);
+        if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
+            expandedInstallDir = existingVal;
+            return expandedInstallDir;
+        }
+
+        String untidiedVal = ConfigToAttributes.apply(getEntity(), SoftwareProcess.EXPANDED_INSTALL_DIR);
+        if (Strings.isNonBlank(untidiedVal)) {
+            setExpandedInstallDir(Os.tidyPath(untidiedVal));
+            return expandedInstallDir;
+        } else {
+            throw new IllegalStateException("expandedInstallDir is null; most likely install was not called for "+getEntity());
+        }
+    }
 }
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessSshDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessSshDriver.java
index 9ff370c..376577b 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessSshDriver.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessSshDriver.java
@@ -81,13 +81,6 @@
     public static final Logger log = LoggerFactory.getLogger(AbstractSoftwareProcessSshDriver.class);
     public static final Logger logSsh = LoggerFactory.getLogger(BrooklynLogging.SSH_IO);
 
-    // we cache these for efficiency and in case the entity becomes unmanaged
-    private volatile String installDir;
-    private volatile String runDir;
-    private volatile String expandedInstallDir;
-
-    private final Object installDirSetupMutex = new Object();
-
     protected volatile DownloadResolver resolver;
 
     @Override
@@ -130,99 +123,6 @@
         return (SshMachineLocation) super.getLocation();
     }
 
-    protected void setInstallDir(String installDir) {
-        this.installDir = installDir;
-        entity.sensors().set(SoftwareProcess.INSTALL_DIR, installDir);
-    }
-
-    @Override
-    public String getInstallDir() {
-        if (installDir != null) return installDir;
-
-        String existingVal = getEntity().getAttribute(SoftwareProcess.INSTALL_DIR);
-        if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
-            installDir = existingVal;
-            return installDir;
-        }
-
-        synchronized (installDirSetupMutex) {
-            // previously we looked at sensor value, but we shouldn't as it might have been converted from the config key value
-            // *before* we computed the install label, or that label may have changed since previous install; now force a recompute
-            setInstallLabel();
-
-            // set it null first so that we force a recompute
-            setInstallDir(null);
-            setInstallDir(Os.tidyPath(ConfigToAttributes.apply(getEntity(), SoftwareProcess.INSTALL_DIR)));
-            return installDir;
-        }
-    }
-
-    protected void setInstallLabel() {
-        if (((EntityInternal)getEntity()).config().getLocalRaw(SoftwareProcess.INSTALL_UNIQUE_LABEL).isPresentAndNonNull()) return;
-        getEntity().config().set(SoftwareProcess.INSTALL_UNIQUE_LABEL,
-            getEntity().getEntityType().getSimpleName()+
-            (Strings.isNonBlank(getVersion()) ? "_"+getVersion() : "")+
-            (Strings.isNonBlank(getInstallLabelExtraSalt()) ? "_"+getInstallLabelExtraSalt() : "") );
-    }
-
-    /** allows subclasses to return extra salt (ie unique hash)
-     * for cases where install dirs need to be distinct e.g. based on extra plugins being placed in the install dir;
-     * {@link #setInstallLabel()} uses entity-type simple name and version already
-     * <p>
-     * this salt should not be too long and must not contain invalid path chars.
-     * a hash code of other relevant info is not a bad choice.
-     **/
-    protected String getInstallLabelExtraSalt() {
-        return null;
-    }
-
-    protected void setRunDir(String runDir) {
-        this.runDir = runDir;
-        entity.sensors().set(SoftwareProcess.RUN_DIR, runDir);
-    }
-
-    @Override
-    public String getRunDir() {
-        if (runDir != null) return runDir;
-
-        String existingVal = getEntity().getAttribute(SoftwareProcess.RUN_DIR);
-        if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
-            runDir = existingVal;
-            return runDir;
-        }
-
-        setRunDir(Os.tidyPath(ConfigToAttributes.apply(getEntity(), SoftwareProcess.RUN_DIR)));
-        return runDir;
-    }
-
-    public void setExpandedInstallDir(String val) {
-        String oldVal = getEntity().getAttribute(SoftwareProcess.EXPANDED_INSTALL_DIR);
-        if (Strings.isNonBlank(oldVal) && !oldVal.equals(val)) {
-            log.info("Resetting expandedInstallDir (to "+val+" from "+oldVal+") for "+getEntity());
-        }
-
-        expandedInstallDir = val;
-        getEntity().sensors().set(SoftwareProcess.EXPANDED_INSTALL_DIR, val);
-    }
-
-    public String getExpandedInstallDir() {
-        if (expandedInstallDir != null) return expandedInstallDir;
-
-        String existingVal = getEntity().getAttribute(SoftwareProcess.EXPANDED_INSTALL_DIR);
-        if (Strings.isNonBlank(existingVal)) { // e.g. on rebind
-            expandedInstallDir = existingVal;
-            return expandedInstallDir;
-        }
-
-        String untidiedVal = ConfigToAttributes.apply(getEntity(), SoftwareProcess.EXPANDED_INSTALL_DIR);
-        if (Strings.isNonBlank(untidiedVal)) {
-            setExpandedInstallDir(Os.tidyPath(untidiedVal));
-            return expandedInstallDir;
-        } else {
-            throw new IllegalStateException("expandedInstallDir is null; most likely install was not called for "+getEntity());
-        }
-    }
-
     public SshMachineLocation getMachine() { return getLocation(); }
     public String getHostname() { return entity.getAttribute(Attributes.HOSTNAME); }
     public String getAddress() { return entity.getAttribute(Attributes.ADDRESS); }
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessWinRmDriver.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessWinRmDriver.java
index 0303313..eca10a1 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessWinRmDriver.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/AbstractSoftwareProcessWinRmDriver.java
@@ -58,7 +58,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Maps;
 
 public abstract class AbstractSoftwareProcessWinRmDriver extends AbstractSoftwareProcessDriver implements NativeWindowsScriptRunner {
     private static final Logger LOG = LoggerFactory.getLogger(AbstractSoftwareProcessWinRmDriver.class);
@@ -74,6 +73,10 @@
         entity.sensors().set(WINDOWS_PASSWORD, location.config().get(WinRmMachineLocation.PASSWORD));
     }
 
+    protected String mergePaths(String ...s) {
+        return super.mergePaths(s).replaceAll("/", "\\\\");
+    }
+
     protected WinRmExecuteHelper newScript(String command, String psCommand, String phase, String taskNamePrefix) {
         return newScript(command, psCommand, phase, taskNamePrefix, null);
     }
@@ -284,28 +287,17 @@
     }
 
     @Override
-    public String getRunDir() {
-        // TODO: This needs to be tidied, and read from the appropriate flags (if set)
-        return "$HOME\\brooklyn-managed-processes\\apps\\" + entity.getApplicationId() + "\\entities\\" + getEntityVersionLabel()+"_"+entity.getId();
-    }
-
-    @Override
-    public String getInstallDir() {
-        // TODO: This needs to be tidied, and read from the appropriate flags (if set)
-        return "$HOME\\brooklyn-managed-processes\\installs\\" + entity.getApplicationId() + "\\" + getEntityVersionLabel()+"_"+entity.getId();
-    }
-
-    @Override
-    public int copyResource(Map<Object, Object> sshFlags, String source, String target, boolean createParentDir) {
+    public int copyResource(Map<Object, Object> sshFlags, String sourceUrl, String target, boolean createParentDir) {
         if (createParentDir) {
             createDirectory(getDirectory(target), "Creating resource directory");
         }
 
         InputStream stream = null;
         try {
-            Tasks.setBlockingDetails("retrieving resource "+source+" for copying across");
-            stream = resource.getResourceFromUrl(source);
-            Tasks.setBlockingDetails("copying resource "+source+" to server");
+            Tasks.setBlockingDetails("retrieving resource "+sourceUrl+" for copying across");
+            stream = resource.getResourceFromUrl(sourceUrl);
+            Tasks.setBlockingDetails("copying resource "+sourceUrl+" to server");
+            LOG.debug("Copying "+sourceUrl+" to "+target+" on "+getLocation()+" for "+getEntity());
             return copyTo(stream, target);
         } catch (Exception e) {
             throw Exceptions.propagate(e);
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java
index e772f24..7879d58 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/VanillaWindowsProcess.java
@@ -30,12 +30,53 @@
 import org.apache.brooklyn.config.ConfigInheritance;
 import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
+import org.apache.brooklyn.core.sensor.AttributeSensorAndConfigKey;
 import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.core.sensor.TemplatedStringAttributeSensorAndConfigKey;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
 import org.apache.brooklyn.util.time.Duration;
 
 @Catalog(name="Vanilla Windows Process", description="A basic Windows entity configured with scripts, e.g. for launch, check-running and stop")
 @ImplementedBy(VanillaWindowsProcessImpl.class)
 public interface VanillaWindowsProcess extends AbstractVanillaProcess {
+
+    @SetFromFlag("installDir")
+    AttributeSensorAndConfigKey<String,String> INSTALL_DIR = new TemplatedStringAttributeSensorAndConfigKey(
+        "install.dir", 
+        "Directory in which this software will be installed (if downloading/unpacking artifacts explicitly); uses FreeMarker templating format",
+        "${" +
+        "config['"+BrooklynConfigKeys.ONBOX_BASE_DIR.getName()+"']!" +
+        "config['"+BrooklynConfigKeys.BROOKLYN_DATA_DIR.getName()+"']!" +
+        "'ERROR-ONBOX_BASE_DIR-not-set'" +
+        "}" +
+        "\\" +
+        "installs\\" +
+        // the  var??  tests if it exists, passing value to ?string(if_present,if_absent)
+        // the ! provides a default value afterwards, which is never used, but is required for parsing
+        // when the config key is not available;
+        // thus the below prefers the install.unique_label, but falls back to simple name
+        // plus a version identifier *if* the version is explicitly set
+        "${(config['install.unique_label']??)?string(config['install.unique_label']!'X'," +
+        "(entity.entityType.simpleName)+" +
+        "((config['install.version']??)?string('_'+(config['install.version']!'X'),''))" +
+        ")}");
+
+    @SetFromFlag("runDir")
+    AttributeSensorAndConfigKey<String,String> RUN_DIR = new TemplatedStringAttributeSensorAndConfigKey(
+        "run.dir", 
+        "Directory from which this software to be run; uses FreeMarker templating format",
+        "${" +
+        "config['"+BrooklynConfigKeys.ONBOX_BASE_DIR.getName()+"']!" +
+        "config['"+BrooklynConfigKeys.BROOKLYN_DATA_DIR.getName()+"']!" +
+        "'ERROR-ONBOX_BASE_DIR-not-set'" +
+        "}" +
+        "\\" +
+        "apps\\${entity.applicationId}\\" +
+        "entities\\${entity.entityType.simpleName}_" +
+        "${entity.id}");
+
+    
     // 3389 is RDP; 5985 is WinRM (3389 isn't used by Brooklyn, but useful for the end-user subsequently)
     ConfigKey<Collection<Integer>> REQUIRED_OPEN_LOGIN_PORTS = ConfigKeys.newConfigKeyWithDefault(
             SoftwareProcess.REQUIRED_OPEN_LOGIN_PORTS,
diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
index 26c9ac4..721c3a0 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/lifecycle/MachineLifecycleEffectorTasks.java
@@ -44,7 +44,6 @@
 import org.apache.brooklyn.core.config.Sanitizer;
 import org.apache.brooklyn.core.effector.EffectorBody;
 import org.apache.brooklyn.core.effector.Effectors;
-import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
 import org.apache.brooklyn.core.entity.Attributes;
 import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
 import org.apache.brooklyn.core.entity.Entities;
@@ -74,6 +73,7 @@
 import org.apache.brooklyn.entity.software.base.SoftwareProcess.StopSoftwareParameters.StopMode;
 import org.apache.brooklyn.entity.stock.EffectorStartableImpl.StartParameters;
 import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.CanResolveOnBoxDir;
 import org.apache.brooklyn.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.collections.MutableSet;
@@ -81,14 +81,11 @@
 import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.core.task.Tasks;
 import org.apache.brooklyn.util.core.task.ValueResolverIterator;
-import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.net.UserAndHostAndPort;
 import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.repeat.Repeater;
-import org.apache.brooklyn.util.ssh.BashCommands;
-import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -561,6 +558,7 @@
         if (base==null) base = machine.getConfig(BrooklynConfigKeys.BROOKLYN_DATA_DIR);
         if (base==null) base = entity.getManagementContext().getConfig().getConfig(BrooklynConfigKeys.BROOKLYN_DATA_DIR);
         if (base==null) base = "~/brooklyn-managed-processes";
+        
         if (base.equals("~")) base=".";
         if (base.startsWith("~/")) base = "."+base.substring(1);
 
@@ -569,21 +567,8 @@
             if (log.isDebugEnabled()) log.debug("Skipping on-box base dir resolution for "+entity+" at "+machine);
             if (!Os.isAbsolutish(base)) base = "~/"+base;
             resolvedBase = Os.tidyPath(base);
-        } else if (machine instanceof SshMachineLocation) {
-            SshMachineLocation ms = (SshMachineLocation)machine;
-            ProcessTaskWrapper<Integer> baseTask = SshEffectorTasks.ssh(
-                BashCommands.alternatives("mkdir -p \"${BASE_DIR}\"",
-                    BashCommands.chain(
-                        BashCommands.sudo("mkdir -p \"${BASE_DIR}\""),
-                        BashCommands.sudo("chown "+ms.getUser()+" \"${BASE_DIR}\""))),
-                "cd ~",
-                "cd ${BASE_DIR}",
-                "echo BASE_DIR_RESULT':'`pwd`:BASE_DIR_RESULT")
-                .environmentVariable("BASE_DIR", base)
-                .requiringExitCodeZero()
-                .summary("initializing on-box base dir "+base).newTask();
-            DynamicTasks.queueIfPossible(baseTask).orSubmitAsync(entity);
-            resolvedBase = Strings.getFragmentBetween(baseTask.block().getStdout(), "BASE_DIR_RESULT:", ":BASE_DIR_RESULT");
+        } else if (machine instanceof CanResolveOnBoxDir) {
+            resolvedBase = ((CanResolveOnBoxDir)machine).resolveOnBoxDirFor(entity, base);
         }
         if (resolvedBase==null) {
             if (!Os.isAbsolutish(base)) base = "~/"+base;
diff --git a/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinRmMachineLocation.java b/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinRmMachineLocation.java
index 9471096..0fd93b7 100644
--- a/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinRmMachineLocation.java
+++ b/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinRmMachineLocation.java
@@ -32,6 +32,7 @@
 
 import javax.annotation.Nullable;
 
+import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.location.MachineDetails;
 import org.apache.brooklyn.api.location.MachineLocation;
 import org.apache.brooklyn.api.location.OsDetails;
@@ -40,19 +41,24 @@
 import org.apache.brooklyn.core.config.ConfigKeys;
 import org.apache.brooklyn.core.config.ConfigUtils;
 import org.apache.brooklyn.core.config.Sanitizer;
+import org.apache.brooklyn.core.effector.ssh.SshEffectorTasks;
 import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
 import org.apache.brooklyn.core.location.AbstractMachineLocation;
 import org.apache.brooklyn.core.location.access.PortForwardManager;
 import org.apache.brooklyn.core.location.access.PortForwardManagerLocationResolver;
 import org.apache.brooklyn.core.mgmt.ManagementContextInjectable;
+import org.apache.brooklyn.location.ssh.CanResolveOnBoxDir;
 import org.apache.brooklyn.util.core.ClassLoaderUtils;
 import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.internal.ssh.SshTool;
 import org.apache.brooklyn.util.core.internal.winrm.WinRmTool;
 import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
 import org.apache.brooklyn.util.core.internal.winrm.winrm4j.Winrm4jTool;
+import org.apache.brooklyn.util.core.task.DynamicTasks;
+import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.ssh.BashCommands;
 import org.apache.brooklyn.util.stream.Streams;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.commons.codec.binary.Base64;
@@ -71,7 +77,7 @@
 import com.google.common.net.HostAndPort;
 import com.google.common.reflect.TypeToken;
 
-public class WinRmMachineLocation extends AbstractMachineLocation implements MachineLocation {
+public class WinRmMachineLocation extends AbstractMachineLocation implements MachineLocation, CanResolveOnBoxDir {
 
     private static final Logger LOG = LoggerFactory.getLogger(WinRmMachineLocation.class);
 
@@ -492,4 +498,13 @@
 //        ));
     }
 
+    @Override
+    public String resolveOnBoxDirFor(Entity entity, String unresolvedPath) {
+        // TODO this is simplistic, writes at c:\ for HOME
+        if (unresolvedPath.startsWith("./") || unresolvedPath.startsWith("~/")) {
+            unresolvedPath = "C:\\"+unresolvedPath.substring(2);
+        }
+        return unresolvedPath.replaceAll("/", "\\");
+    }
+
 }
diff --git a/software/winrm/src/main/java/org/apache/brooklyn/util/core/internal/winrm/winrm4j/Winrm4jTool.java b/software/winrm/src/main/java/org/apache/brooklyn/util/core/internal/winrm/winrm4j/Winrm4jTool.java
index 2ea8318..b7ff665 100644
--- a/software/winrm/src/main/java/org/apache/brooklyn/util/core/internal/winrm/winrm4j/Winrm4jTool.java
+++ b/software/winrm/src/main/java/org/apache/brooklyn/util/core/internal/winrm/winrm4j/Winrm4jTool.java
@@ -144,7 +144,12 @@
             byte[] inputData = new byte[chunkSize];
             int bytesRead;
             int expectedFileSize = 0;
+            int i=0;
             while ((bytesRead = source.read(inputData)) > 0) {
+                i++;
+                
+                LOG.debug("Copying chunk "+i+" to "+destination+" on "+host);
+                
                 byte[] chunk;
                 if (bytesRead == chunkSize) {
                     chunk = inputData;
@@ -156,7 +161,8 @@
                         " -value ([System.Convert]::FromBase64String(\"" + new String(Base64.encodeBase64(chunk)) + "\"))}"));
                 expectedFileSize += bytesRead;
             }
-
+            LOG.debug("Finished copying to "+destination+" on "+host);
+            
             return new org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse("", "", 0);
         } catch (java.io.IOException e) {
             throw propagate(e, "Failed copying to server at "+destination);