1 /*
2  *Copyright (C) 2018 Laurent Tréguier
3  *
4  *This file is part of DLS.
5  *
6  *DLS is free software: you can redistribute it and/or modify
7  *it under the terms of the GNU General Public License as published by
8  *the Free Software Foundation, either version 3 of the License, or
9  *(at your option) any later version.
10  *
11  *DLS is distributed in the hope that it will be useful,
12  *but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *GNU General Public License for more details.
15  *
16  *You should have received a copy of the GNU General Public License
17  *along with DLS.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 module dls.updater;
22 
23 import std.format : format;
24 
25 private enum descriptionJson = import("description.json");
26 private immutable changelogUrl = format!"https://github.com/d-language-server/dls/blob/v%s/CHANGELOG.md"(
27         currentVersion);
28 
29 void cleanup()
30 {
31     import dls.bootstrap : dubBinDir;
32     import dub.semver : compareVersions;
33     import std.file : FileException, SpanMode, dirEntries, isSymlink, remove,
34         rmdirRecurse;
35     import std.path : baseName;
36     import std.regex : matchFirst;
37 
38     foreach (string entry; dirEntries(dubBinDir, SpanMode.shallow))
39     {
40         const match = entry.baseName.matchFirst(`dls-v([\d.]+)`);
41 
42         if (match)
43         {
44             if (compareVersions(currentVersion, match[1]) > 0)
45             {
46                 try
47                 {
48                     rmdirRecurse(entry);
49                 }
50                 catch (FileException e)
51                 {
52                 }
53             }
54         }
55         else if (isSymlink(entry))
56         {
57             try
58             {
59                 version (Windows)
60                 {
61                     import std.file : isDir, rmdir;
62                     import std.stdio : File;
63 
64                     if (isDir(entry))
65                     {
66                         try
67                         {
68                             dirEntries(entry, SpanMode.shallow);
69                         }
70                         catch (FileException e)
71                         {
72                             rmdir(entry);
73                         }
74                     }
75                     else
76                     {
77                         try
78                         {
79                             File(entry, "rb");
80                         }
81                         catch (Exception e)
82                         {
83                             remove(entry);
84                         }
85                     }
86                 }
87                 else version (Posix)
88                 {
89                     import std.file : exists, readLink;
90 
91                     if (!exists(readLink(entry)))
92                     {
93                         remove(entry);
94                     }
95                 }
96             }
97             catch (Exception e)
98             {
99             }
100         }
101     }
102 }
103 
104 void update(bool autoUpdate)
105 {
106     import core.time : hours;
107     import dls.bootstrap : UpgradeFailedException, apiEndpoint, buildDls,
108         canDownloadDls, downloadDls, linkDls;
109     static import dls.protocol.jsonrpc;
110     import dls.protocol.interfaces.dls : DlsUpgradeSizeParams,
111         TranslationParams;
112     import dls.protocol.messages.methods : Dls;
113     import dls.protocol.messages.window : Util;
114     import dls.util.constants : Tr;
115     import dls.util.logger : logger;
116     import dls.util.path : normalized;
117     import dub.dependency : Dependency;
118     import dub.dub : Dub, FetchOptions;
119     import dub.semver : compareVersions;
120     import std.algorithm : stripLeft;
121     import std.concurrency : ownerTid, receiveOnly, register, send, thisTid;
122     import std.datetime : Clock, SysTime;
123     import std.json : parseJSON;
124     import std.net.curl : get;
125 
126     const latestRelease = parseJSON(get(format!apiEndpoint("releases/latest")));
127     const latestVersion = latestRelease["tag_name"].str.stripLeft('v');
128     const releaseTime = SysTime.fromISOExtString(latestRelease["published_at"].str);
129 
130     if (latestVersion.length == 0 || compareVersions(currentVersion,
131             latestVersion) >= 0 || (Clock.currTime.toUTC() - releaseTime < 1.hours))
132     {
133         return;
134     }
135 
136     if (!autoUpdate)
137     {
138         auto id = Util.sendMessageRequest(Tr.app_upgradeDls,
139                 [Tr.app_upgradeDls_upgrade], [latestVersion, currentVersion]);
140         const threadName = "updater";
141         register(threadName, thisTid());
142         send(ownerTid(), Util.ThreadMessageData(id, Tr.app_upgradeDls, threadName));
143 
144         const shouldUpgrade = receiveOnly!bool();
145 
146         if (!shouldUpgrade)
147         {
148             return;
149         }
150     }
151 
152     dls.protocol.jsonrpc.send(Dls.UpgradeDls.didStart,
153             new TranslationParams(Tr.app_upgradeDls_upgrading));
154 
155     scope (exit)
156     {
157         dls.protocol.jsonrpc.send(Dls.UpgradeDls.didStop);
158     }
159 
160     bool upgradeSuccessful;
161 
162     if (canDownloadDls)
163     {
164         try
165         {
166             enum totalSizeCallback = (size_t size) {
167                 dls.protocol.jsonrpc.send(Dls.UpgradeDls.didChangeTotalSize,
168                         new DlsUpgradeSizeParams(Tr.app_upgradeDls_downloading, [], size));
169             };
170             enum chunkSizeCallback = (size_t size) {
171                 dls.protocol.jsonrpc.send(Dls.UpgradeDls.didChangeCurrentSize,
172                         new DlsUpgradeSizeParams(Tr.app_upgradeDls_downloading, [], size));
173             };
174             enum extractCallback = () {
175                 dls.protocol.jsonrpc.send(Dls.UpgradeDls.didExtract,
176                         new TranslationParams(Tr.app_upgradeDls_extracting));
177             };
178 
179             downloadDls(totalSizeCallback, chunkSizeCallback, extractCallback);
180             upgradeSuccessful = true;
181         }
182         catch (Exception e)
183         {
184             logger.errorf("Could not download DLS: %s", e.message);
185         }
186     }
187 
188     if (!upgradeSuccessful)
189     {
190         auto dub = new Dub();
191         FetchOptions fetchOpts;
192         fetchOpts |= FetchOptions.forceBranchUpgrade;
193         const pack = dub.fetch("dls", Dependency(">=0.0.0"),
194                 dub.defaultPlacementLocation, fetchOpts);
195 
196         int i;
197         const additionalArgs = [[], ["--force"]];
198 
199         do
200         {
201             try
202             {
203                 buildDls(pack.path.toString().normalized, additionalArgs[i]);
204                 upgradeSuccessful = true;
205             }
206             catch (UpgradeFailedException e)
207             {
208                 ++i;
209             }
210         }
211         while (i < additionalArgs.length && !upgradeSuccessful);
212 
213         if (!upgradeSuccessful)
214         {
215             Util.sendMessage(Tr.app_buildError);
216             return;
217         }
218     }
219 
220     try
221     {
222         linkDls();
223         auto id = Util.sendMessageRequest(Tr.app_showChangelog,
224                 [Tr.app_showChangelog_show], [latestVersion]);
225         send(ownerTid(), Util.ThreadMessageData(id, Tr.app_showChangelog, changelogUrl));
226     }
227     catch (UpgradeFailedException e)
228     {
229         Util.sendMessage(Tr.app_linkError);
230     }
231 }
232 
233 @property private string currentVersion()
234 {
235     import std.algorithm : find;
236     import std.json : parseJSON;
237 
238     const desc = parseJSON(descriptionJson);
239     return desc["packages"].array.find!(p => p["name"] == desc["rootPackage"])[0]["version"].str;
240 }