Skip to content

Commit 4911c46

Browse files
danvkCopilot
andauthored
Detach process (#243)
* working on toy * subprocess.Popen works * port to app * copy before detaching * print message in main process * quiet server logging * fix regex warning * update message and notes * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent f723977 commit 4911c46

File tree

7 files changed

+83
-22
lines changed

7 files changed

+83
-22
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ To iterate on the PyPI package, run:
144144

145145
pip3 uninstall webdiff
146146
poetry build
147-
pip3 install dist/webdiff-?.?.?.tar.gz
147+
pip3 install dist/webdiff-(latest).tar.gz
148148

149149
To publish to pypitest:
150150

@@ -155,6 +155,8 @@ And to the real pypi:
155155

156156
poetry publish
157157

158+
You can publish pre-release versions to pypi by adding "bN" to the version number.
159+
158160
See [pypirc][] and [poetry][] docs for details on setting up tokens for pypi.
159161

160162
Publication checklist. Do these from _outside_ the webdiff directory:
@@ -184,7 +186,10 @@ When you run `git webdiff (args)`, it runs:
184186

185187
This tells `git` to set up two directories and invoke `webdiff leftdir rightdir`.
186188

187-
There's one complication involving symlinks. `git difftool -d` may fill one of the sides (typically the right) with symlinks. This is faster than copying files, but unfortunately `git diff --no-index` does not resolve these symlinks. To make this work, if a directory contains symlinks, webdiff makes a copy of it before diffing. For file diffs, it resolves the symlink before passing it to `git diff --no-index`. The upshot is that you can run `git webdiff`, edit a file, reload the browser window and see the changes.
189+
There are two wrinkles here:
190+
191+
- `git difftool -d` may fill one of the sides (typically the right) with symlinks. This is faster than copying files, but unfortunately `git diff --no-index` does not resolve these symlinks. To make this work, if a directory contains symlinks, webdiff makes a copy of it before diffing. For file diffs, it resolves the symlink before passing it to `git diff --no-index`. The upshot is that you can run `git webdiff`, edit a file, reload the browser window and see the changes.
192+
- `git difftool` cleans up its temporary directories when the main webdiff process terminates. Since webdiff detaches to give you back your terminal, it has to make another copy of the directories (this time without resolving symlinks) to make sure they're still there for the child process.
188193

189194
[pypirc]: https://packaging.python.org/specifications/pypirc/
190195
[Homebrew]: https://brew.sh/

webdiff/app.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import platform
1414
import signal
1515
import socket
16+
import subprocess
1617
import sys
1718
import threading
1819
import time
@@ -24,6 +25,7 @@
2425
from binaryornot.check import is_binary
2526

2627
from webdiff import argparser, diff, options, util
28+
from webdiff.dirdiff import make_resolved_dir
2729

2830
VERSION = importlib.metadata.version('webdiff')
2931

@@ -46,6 +48,7 @@ def determine_path():
4648
PORT = None
4749
HOSTNAME = 'localhost'
4850
DEBUG = os.environ.get('DEBUG')
51+
DEBUG_DETACH = os.environ.get('DEBUG_DETACH')
4952
WEBDIFF_DIR = determine_path()
5053

5154
if DEBUG:
@@ -273,15 +276,8 @@ def pick_a_port(args, webdiff_config):
273276

274277

275278
def run_http():
276-
sys.stderr.write(
277-
"""Serving diffs on http://%s:%s
278-
Close the browser tab or hit Ctrl-C when you're done.
279-
"""
280-
% (HOSTNAME, PORT)
281-
)
282279
threading.Timer(0.1, open_browser).start()
283-
284-
web.run_app(app, host=HOSTNAME, port=PORT)
280+
web.run_app(app, host=HOSTNAME, port=PORT, print=print if DEBUG else None)
285281
logging.debug('http server shut down')
286282

287283

@@ -291,7 +287,7 @@ def maybe_shutdown():
291287

292288
def shutdown():
293289
if LAST_REQUEST_MS <= last_ms: # subsequent requests abort shutdown
294-
sys.stderr.write('Shutting down...\n')
290+
logging.debug('Shutting down...')
295291
signal.raise_signal(signal.SIGINT)
296292
else:
297293
logging.debug('Received subsequent request; shutdown aborted.')
@@ -334,7 +330,36 @@ def run():
334330
else:
335331
HOSTNAME = _hostname
336332

337-
run_http()
333+
run_in_process = os.environ.get('WEBDIFF_RUN_IN_PROCESS') or (
334+
DEBUG and not DEBUG_DETACH
335+
)
336+
337+
if not os.environ.get('WEBDIFF_LOGGED_MESSAGE'):
338+
# Printing this in the main process gives you your prompt back more cleanly.
339+
print(
340+
"""Serving diffs on http://%s:%s
341+
Close the browser tab when you're done to terminate the process."""
342+
% (HOSTNAME, PORT)
343+
)
344+
os.environ['WEBDIFF_LOGGED_MESSAGE'] = '1'
345+
346+
if run_in_process:
347+
run_http()
348+
else:
349+
os.environ['WEBDIFF_RUN_IN_PROCESS'] = '1'
350+
os.environ['WEBDIFF_PORT'] = str(PORT)
351+
if os.environ.get('WEBDIFF_FROM_GIT_DIFFTOOL'):
352+
# git difftool will clean up these directories when we detach.
353+
# To make them accessible to the child process, we make a (shallow) copy.
354+
assert 'dirs' in parsed_args
355+
dir_a, dir_b = parsed_args['dirs']
356+
copied_dir_a = make_resolved_dir(dir_a)
357+
copied_dir_b = make_resolved_dir(dir_b)
358+
os.environ['WEBDIFF_DIR_A'] = copied_dir_a
359+
os.environ['WEBDIFF_DIR_B'] = copied_dir_b
360+
logging.debug(f'Copied {dir_a} -> {copied_dir_a} before detaching')
361+
logging.debug(f'Copied {dir_b} -> {copied_dir_b} before detaching')
362+
subprocess.Popen((sys.executable, *sys.argv))
338363

339364

340365
if __name__ == '__main__':

webdiff/argparser.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import os
55
import re
66

7-
from webdiff import dirdiff
8-
from webdiff import githubdiff
9-
from webdiff import github_fetcher
7+
from webdiff import dirdiff, github_fetcher, githubdiff
108
from webdiff.localfilediff import LocalFileDiff
119

1210

@@ -81,6 +79,12 @@ def parse(args, version=None):
8179

8280
else:
8381
a, b = args.dirs
82+
if os.environ.get('WEBDIFF_DIR_A') and os.environ.get('WEBDIFF_DIR_B'):
83+
# This happens when you run "git webdiff" and we have to make a copy of
84+
# the temp directories before we detach and git difftool cleans them up.
85+
a = os.environ.get('WEBDIFF_DIR_A')
86+
b = os.environ.get('WEBDIFF_DIR_B')
87+
8488
for x in (a, b):
8589
if not os.path.exists(x):
8690
raise UsageError('"%s" does not exist' % x)

webdiff/dirdiff.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Compute the diff between two directories on local disk."""
22

3-
import os
43
import logging
4+
import os
55
import shutil
66
import subprocess
77
import tempfile
@@ -27,7 +27,7 @@ def contains_symlinks(dir: str):
2727
return False
2828

2929

30-
def make_resolved_dir(dir: str) -> str:
30+
def make_resolved_dir(dir: str, follow_symlinks=False) -> str:
3131
# TODO: clean up this directory
3232
temp_dir = tempfile.mkdtemp(prefix='webdiff')
3333
for root, dirs, files in os.walk(dir):
@@ -38,7 +38,7 @@ def make_resolved_dir(dir: str) -> str:
3838
src_file = os.path.join(root, file_name)
3939
rel = os.path.relpath(src_file, dir)
4040
dst_file = os.path.join(temp_dir, rel)
41-
shutil.copy(src_file, dst_file, follow_symlinks=True)
41+
shutil.copy(src_file, dst_file, follow_symlinks=follow_symlinks)
4242
return temp_dir
4343

4444

@@ -49,11 +49,11 @@ def gitdiff(a_dir: str, b_dir: str, webdiff_config):
4949
cmd += ' ' + extra_args
5050
a_dir_nosym = a_dir
5151
if contains_symlinks(a_dir):
52-
a_dir_nosym = make_resolved_dir(a_dir)
52+
a_dir_nosym = make_resolved_dir(a_dir, follow_symlinks=True)
5353
logging.debug(f'Inlined symlinks in left directory {a_dir} -> {a_dir_nosym}')
5454
b_dir_nosym = b_dir
5555
if contains_symlinks(b_dir):
56-
b_dir_nosym = make_resolved_dir(b_dir)
56+
b_dir_nosym = make_resolved_dir(b_dir, follow_symlinks=True)
5757
logging.debug(f'Inlined symlinks in right directory {b_dir} -> {b_dir_nosym}')
5858
args = cmd.split(' ') + [a_dir_nosym, b_dir_nosym]
5959
logging.debug('Running git command: %s', args)

webdiff/github_fetcher.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
# Use this PR for testing to see all four types of change at once:
77
# https://github.com/danvk/test-repo/pull/2/
88

9-
from collections import OrderedDict
109
import os
1110
import re
1211
import subprocess
1312
import sys
13+
from collections import OrderedDict
1414

1515
from github import Github, UnknownObjectException
1616

@@ -138,7 +138,7 @@ def parse(remote):
138138

139139
# e.g. 'origin [email protected]:danvk/expandable-image-grid.git (push)'
140140
ssh_push_re = re.compile(
141-
'(?P<name>[^\s]+)\s+((?P<user>[^@]+)@)?(?P<host>[^:]+)(?::(?P<path>[^\s]+))?\s\(push\)'
141+
r'(?P<name>[^\s]+)\s+((?P<user>[^@]+)@)?(?P<host>[^:]+)(?::(?P<path>[^\s]+))?\s\(push\)'
142142
)
143143

144144
# e.g. 'origin https://github.com/danvk/git-helpers.git (push)'

webdiff/gitwebdiff.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def run(argv=sys.argv):
2121
if not os.environ.get('DEBUG')
2222
else os.path.join(os.path.curdir, 'test.sh')
2323
)
24+
os.environ['WEBDIFF_FROM_GIT_DIFFTOOL'] = '1'
2425
subprocess.call(f'git difftool -d -x {cmd}'.split(' ') + argv[1:])
2526
except KeyboardInterrupt:
2627
# Don't raise an exception to the user when sigint is received

webdiff/toy.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Trying to make a server that detaches
2+
import os
3+
import subprocess
4+
import sys
5+
import time
6+
7+
8+
def run():
9+
print('running server...')
10+
print(f'{sys.argv=}')
11+
print(f'{os.getpid()=}')
12+
time.sleep(3)
13+
print('shutting down.')
14+
15+
16+
def main():
17+
if os.environ.get('SUB'):
18+
run()
19+
else:
20+
os.environ['SUB'] = '1'
21+
subprocess.Popen((sys.executable, *sys.argv))
22+
print('terminating parent process')
23+
24+
25+
if __name__ == '__main__':
26+
main()

0 commit comments

Comments
 (0)